istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/bootstrap/config.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 bootstrap 16 17 import ( 18 "encoding/json" 19 "errors" 20 "fmt" 21 "os" 22 "path" 23 "sort" 24 "strconv" 25 "strings" 26 27 core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 28 "google.golang.org/protobuf/types/known/structpb" 29 "google.golang.org/protobuf/types/known/wrapperspb" 30 31 "istio.io/api/annotation" 32 meshAPI "istio.io/api/mesh/v1alpha1" 33 "istio.io/istio/pilot/pkg/features" 34 "istio.io/istio/pilot/pkg/util/network" 35 "istio.io/istio/pkg/bootstrap/option" 36 "istio.io/istio/pkg/bootstrap/platform" 37 "istio.io/istio/pkg/config/constants" 38 "istio.io/istio/pkg/env" 39 common_features "istio.io/istio/pkg/features" 40 "istio.io/istio/pkg/kube/labels" 41 "istio.io/istio/pkg/log" 42 "istio.io/istio/pkg/model" 43 "istio.io/istio/pkg/security" 44 "istio.io/istio/pkg/util/protomarshal" 45 "istio.io/istio/pkg/util/sets" 46 "istio.io/istio/pkg/version" 47 ) 48 49 const ( 50 // IstioMetaPrefix is used to pass env vars as node metadata. 51 IstioMetaPrefix = "ISTIO_META_" 52 53 // IstioMetaJSONPrefix is used to pass annotations and similar environment info. 54 IstioMetaJSONPrefix = "ISTIO_METAJSON_" 55 56 lightstepAccessTokenBase = "lightstep_access_token.txt" 57 58 // required stats are used by readiness checks. 59 requiredEnvoyStatsMatcherInclusionPrefixes = "cluster_manager,listener_manager,server,cluster.xds-grpc,wasm" 60 61 rbacEnvoyStatsMatcherInclusionSuffix = "rbac.allowed,rbac.denied,shadow_allowed,shadow_denied" 62 63 requiredEnvoyStatsMatcherInclusionSuffixes = rbacEnvoyStatsMatcherInclusionSuffix + ",downstream_cx_active" // Needed for draining. 64 65 // required for metrics based on stat_prefix in virtual service. 66 requiredEnvoyStatsMatcherInclusionRegexes = `vhost\.*\.route\.*` 67 68 // Prefixes of V2 metrics. 69 // "reporter" prefix is for istio standard metrics. 70 // "component" suffix is for istio_build metric. 71 v2Prefixes = "reporter=," 72 v2Suffix = ",component,istio" 73 ) 74 75 var envoyWellKnownCompressorLibrary = sets.String{ 76 "gzip": {}, 77 "zstd": {}, 78 "brotli": {}, 79 } 80 81 // Config for creating a bootstrap file. 82 type Config struct { 83 *model.Node 84 // CompliancePolicy to decouple the environment variable dependency. 85 CompliancePolicy string 86 LogAsJSON bool 87 } 88 89 // toTemplateParams creates a new template configuration for the given configuration. 90 func (cfg Config) toTemplateParams() (map[string]any, error) { 91 opts := make([]option.Instance, 0) 92 93 discHost := strings.Split(cfg.Metadata.ProxyConfig.DiscoveryAddress, ":")[0] 94 95 xdsType := "GRPC" 96 if features.DeltaXds { 97 xdsType = "DELTA_GRPC" 98 } 99 100 // Waypoint overrides 101 metadataDiscovery := cfg.Metadata.MetadataDiscovery 102 if strings.HasPrefix(cfg.ID, "waypoint~") { 103 xdsType = "DELTA_GRPC" 104 metadataDiscovery = true 105 } 106 107 opts = append(opts, 108 option.NodeID(cfg.ID), 109 option.NodeType(cfg.ID), 110 option.PilotSubjectAltName(cfg.Metadata.PilotSubjectAltName), 111 option.OutlierLogPath(cfg.Metadata.OutlierLogPath), 112 option.ApplicationLogJSON(cfg.LogAsJSON), 113 option.DiscoveryHost(discHost), 114 option.Metadata(cfg.Metadata), 115 option.XdsType(xdsType), 116 option.MetadataDiscovery(bool(metadataDiscovery)), 117 option.MetricsLocalhostAccessOnly(cfg.Metadata.ProxyConfig.ProxyMetadata), 118 option.DeferredClusterCreation(features.EnableDeferredClusterCreation)) 119 120 // Add GCPProjectNumber to access in bootstrap template. 121 md := cfg.Metadata.PlatformMetadata 122 if projectNumber, found := md[platform.GCPProjectNumber]; found { 123 opts = append(opts, option.GCPProjectNumber(projectNumber)) 124 } 125 126 if cfg.Metadata.StsPort != "" { 127 stsPort, err := strconv.Atoi(cfg.Metadata.StsPort) 128 if err == nil && stsPort > 0 { 129 opts = append(opts, 130 option.STSEnabled(true), 131 option.STSPort(stsPort)) 132 md := cfg.Metadata.PlatformMetadata 133 if projectID, found := md[platform.GCPProject]; found { 134 opts = append(opts, option.GCPProjectID(projectID)) 135 } 136 } 137 } 138 139 // Support passing extra info from node environment as metadata 140 opts = append(opts, getNodeMetadataOptions(cfg.Node, cfg.CompliancePolicy)...) 141 142 // Check if nodeIP carries IPv4 or IPv6 and set up proxy accordingly 143 if network.AllIPv4(cfg.Metadata.InstanceIPs) { 144 // IPv4 only 145 opts = append(opts, 146 option.Localhost(option.LocalhostIPv4), 147 option.Wildcard(option.WildcardIPv4), 148 option.DNSLookupFamily(option.DNSLookupFamilyIPv4)) 149 } else if network.AllIPv6(cfg.Metadata.InstanceIPs) { 150 // IPv6 only 151 opts = append(opts, 152 option.Localhost(option.LocalhostIPv6), 153 option.Wildcard(option.WildcardIPv6), 154 option.DNSLookupFamily(option.DNSLookupFamilyIPv6)) 155 } else { 156 // Dual Stack 157 if features.EnableDualStack { 158 // If dual-stack, it may be [IPv4, IPv6] or [IPv6, IPv4] 159 // So let the first ip family policy to decide its DNSLookupFamilyIP policy 160 ipFamily, err := network.CheckIPFamilyTypeForFirstIPs(cfg.Metadata.InstanceIPs) 161 if err != nil { 162 return nil, err 163 } 164 if ipFamily == network.IPv6 { 165 opts = append(opts, 166 option.Localhost(option.LocalhostIPv6), 167 option.AdditionalLocalhost(option.LocalhostIPv4), 168 option.Wildcard(option.WildcardIPv6), 169 option.AdditionalWildCard(option.WildcardIPv4), 170 option.DNSLookupFamily(option.DNSLookupFamilyIPS)) 171 } else { 172 opts = append(opts, 173 option.Localhost(option.LocalhostIPv4), 174 option.AdditionalLocalhost(option.LocalhostIPv6), 175 option.Wildcard(option.WildcardIPv4), 176 option.AdditionalWildCard(option.WildcardIPv6), 177 option.DNSLookupFamily(option.DNSLookupFamilyIPS)) 178 } 179 opts = append(opts, option.DualStack(true)) 180 } else { 181 // keep the original logic if Dual Stack is disabled 182 opts = append(opts, 183 option.Localhost(option.LocalhostIPv4), 184 option.Wildcard(option.WildcardIPv4), 185 option.DNSLookupFamily(option.DNSLookupFamilyIPv4)) 186 } 187 } 188 189 proxyOpts, err := getProxyConfigOptions(cfg.Metadata) 190 if err != nil { 191 return nil, err 192 } 193 opts = append(opts, proxyOpts...) 194 195 // Append LRS related options. 196 opts = append(opts, option.LoadStatsConfigJSONStr(cfg.Node)) 197 198 // TODO: allow reading a file with additional metadata (for example if created with 199 // 'envref'. This will allow Istio to generate the right config even if the pod info 200 // is not available (in particular in some multi-cluster cases) 201 return option.NewTemplateParams(opts...) 202 } 203 204 // substituteValues substitutes variables known to the bootstrap like pod_ip. 205 // "http.{pod_ip}_" with pod_id = [10.3.3.3,10.4.4.4] --> [http.10.3.3.3_,http.10.4.4.4_] 206 func substituteValues(patterns []string, varName string, values []string) []string { 207 ret := make([]string, 0, len(patterns)) 208 for _, pattern := range patterns { 209 if !strings.Contains(pattern, varName) { 210 ret = append(ret, pattern) 211 continue 212 } 213 214 for _, val := range values { 215 ret = append(ret, strings.Replace(pattern, varName, val, -1)) 216 } 217 } 218 return ret 219 } 220 221 func getStatsOptions(meta *model.BootstrapNodeMetadata) []option.Instance { 222 nodeIPs := meta.InstanceIPs 223 config := meta.ProxyConfig 224 225 tagAnno := meta.Annotations[annotation.SidecarExtraStatTags.Name] 226 prefixAnno := meta.Annotations[annotation.SidecarStatsInclusionPrefixes.Name] 227 RegexAnno := meta.Annotations[annotation.SidecarStatsInclusionRegexps.Name] 228 suffixAnno := meta.Annotations[annotation.SidecarStatsInclusionSuffixes.Name] 229 230 parseOption := func(metaOption string, required string, proxyConfigOption []string) []string { 231 var inclusionOption []string 232 if len(metaOption) > 0 { 233 inclusionOption = strings.Split(metaOption, ",") 234 } else if proxyConfigOption != nil { 235 // In case user relies on mixed usage of annotation and proxy config, 236 // only consider proxy config if annotation is not set instead of merging. 237 inclusionOption = proxyConfigOption 238 } 239 240 if len(required) > 0 { 241 inclusionOption = append(inclusionOption, strings.Split(required, ",")...) 242 } 243 244 // At the sidecar we can limit downstream metrics collection to the inbound listener. 245 // Inbound downstream metrics are named as: http.{pod_ip}_{port}.downstream_rq_* 246 // Other outbound downstream metrics are numerous and not very interesting for a sidecar. 247 // specifying http.{pod_ip}_ as a prefix will capture these downstream metrics. 248 return substituteValues(inclusionOption, "{pod_ip}", nodeIPs) 249 } 250 251 extraStatTags := make([]string, 0, len(config.ExtraStatTags)) 252 for _, tag := range config.ExtraStatTags { 253 if tag != "" { 254 extraStatTags = append(extraStatTags, tag) 255 } 256 } 257 for _, tag := range strings.Split(tagAnno, ",") { 258 if tag != "" { 259 extraStatTags = append(extraStatTags, tag) 260 } 261 } 262 extraStatTags = removeDuplicates(extraStatTags) 263 264 var proxyConfigPrefixes, proxyConfigSuffixes, proxyConfigRegexps []string 265 if config.ProxyStatsMatcher != nil { 266 proxyConfigPrefixes = config.ProxyStatsMatcher.InclusionPrefixes 267 proxyConfigSuffixes = config.ProxyStatsMatcher.InclusionSuffixes 268 proxyConfigRegexps = config.ProxyStatsMatcher.InclusionRegexps 269 } 270 inclusionSuffixes := rbacEnvoyStatsMatcherInclusionSuffix 271 if meta.ExitOnZeroActiveConnections { 272 inclusionSuffixes = requiredEnvoyStatsMatcherInclusionSuffixes 273 } 274 275 var buckets []option.HistogramBucket 276 if bucketsAnno, ok := meta.Annotations[annotation.SidecarStatsHistogramBuckets.Name]; ok { 277 js := map[string][]float64{} 278 err := json.Unmarshal([]byte(bucketsAnno), &js) 279 if err == nil { 280 for prefix, value := range js { 281 buckets = append(buckets, option.HistogramBucket{Match: option.HistogramMatch{Prefix: prefix}, Buckets: value}) 282 } 283 sort.Slice(buckets, func(i, j int) bool { 284 return buckets[i].Match.Prefix < buckets[j].Match.Prefix 285 }) 286 } else { 287 log.Warnf("Failed to unmarshal histogram buckets: %v", bucketsAnno, err) 288 } 289 } 290 291 var compression string 292 // TODO: move annotation to api repo 293 if statsCompression, ok := meta.Annotations["sidecar.istio.io/statsCompression"]; ok && envoyWellKnownCompressorLibrary.Contains(statsCompression) { 294 compression = statsCompression 295 } 296 297 return []option.Instance{ 298 option.EnvoyStatsMatcherInclusionPrefix(parseOption(prefixAnno, 299 requiredEnvoyStatsMatcherInclusionPrefixes, proxyConfigPrefixes)), 300 option.EnvoyStatsMatcherInclusionSuffix(parseOption(suffixAnno, 301 inclusionSuffixes, proxyConfigSuffixes)), 302 option.EnvoyStatsMatcherInclusionRegexp(parseOption(RegexAnno, requiredEnvoyStatsMatcherInclusionRegexes, proxyConfigRegexps)), 303 option.EnvoyExtraStatTags(extraStatTags), 304 option.EnvoyHistogramBuckets(buckets), 305 option.EnvoyStatsCompression(compression), 306 } 307 } 308 309 func lightstepAccessTokenFile(config string) string { 310 return path.Join(config, lightstepAccessTokenBase) 311 } 312 313 func getNodeMetadataOptions(node *model.Node, policy string) []option.Instance { 314 // Add locality options. 315 opts := getLocalityOptions(node.Locality) 316 317 opts = append(opts, getStatsOptions(node.Metadata)...) 318 319 opts = append(opts, 320 option.NodeMetadata(node.Metadata, node.RawMetadata), 321 option.RuntimeFlags(extractRuntimeFlags(node.Metadata.ProxyConfig, policy)), 322 option.EnvoyStatusPort(node.Metadata.EnvoyStatusPort), 323 option.EnvoyPrometheusPort(node.Metadata.EnvoyPrometheusPort)) 324 return opts 325 } 326 327 var StripFragment = env.Register("HTTP_STRIP_FRAGMENT_FROM_PATH_UNSAFE_IF_DISABLED", true, "").Get() 328 329 func extractRuntimeFlags(cfg *model.NodeMetaProxyConfig, policy string) map[string]any { 330 // Setup defaults 331 runtimeFlags := map[string]any{ 332 "overload.global_downstream_max_connections": "2147483647", 333 "re2.max_program_size.error_level": "32768", 334 "envoy.deprecated_features:envoy.config.listener.v3.Listener.hidden_envoy_deprecated_use_original_dst": true, 335 "envoy.reloadable_features.http_reject_path_with_fragment": false, 336 } 337 if policy == common_features.FIPS_140_2 { 338 // This flag limits google_grpc client in Envoy to TLSv1.2 as the maximum version. 339 runtimeFlags["envoy.reloadable_features.google_grpc_disable_tls_13"] = true 340 } 341 if !StripFragment { 342 // Note: the condition here is basically backwards. This was a mistake in the initial commit and cannot be reverted 343 runtimeFlags["envoy.reloadable_features.http_strip_fragment_from_path_unsafe_if_disabled"] = "false" 344 } 345 for k, v := range cfg.RuntimeValues { 346 if v == "" { 347 // Envoy runtime doesn't see "" as a special value, so we use it to mean 'unset default flag' 348 delete(runtimeFlags, k) 349 continue 350 } 351 // Envoy used to allow everything as string but stopped in https://github.com/envoyproxy/envoy/issues/27434 352 // However, our API always takes in strings. 353 // Convert strings to bools for backwards compat. 354 switch v { 355 case "false": 356 runtimeFlags[k] = false 357 case "true": 358 runtimeFlags[k] = true 359 default: 360 runtimeFlags[k] = v 361 } 362 } 363 return runtimeFlags 364 } 365 366 func getLocalityOptions(l *core.Locality) []option.Instance { 367 return []option.Instance{option.Region(l.Region), option.Zone(l.Zone), option.SubZone(l.SubZone)} 368 } 369 370 func getServiceCluster(metadata *model.BootstrapNodeMetadata) string { 371 switch name := metadata.ProxyConfig.ClusterName.(type) { 372 case *meshAPI.ProxyConfig_ServiceCluster: 373 return serviceClusterOrDefault(name.ServiceCluster, metadata) 374 375 case *meshAPI.ProxyConfig_TracingServiceName_: 376 workloadName := metadata.WorkloadName 377 if workloadName == "" { 378 workloadName = "istio-proxy" 379 } 380 381 switch name.TracingServiceName { 382 case meshAPI.ProxyConfig_APP_LABEL_AND_NAMESPACE: 383 return serviceClusterOrDefault("istio-proxy", metadata) 384 case meshAPI.ProxyConfig_CANONICAL_NAME_ONLY: 385 cs, _ := labels.CanonicalService(metadata.Labels, workloadName) 386 return serviceClusterOrDefault(cs, metadata) 387 case meshAPI.ProxyConfig_CANONICAL_NAME_AND_NAMESPACE: 388 cs, _ := labels.CanonicalService(metadata.Labels, workloadName) 389 if metadata.Namespace != "" { 390 return cs + "." + metadata.Namespace 391 } 392 return serviceClusterOrDefault(cs, metadata) 393 default: 394 return serviceClusterOrDefault("istio-proxy", metadata) 395 } 396 397 default: 398 return serviceClusterOrDefault("istio-proxy", metadata) 399 } 400 } 401 402 func serviceClusterOrDefault(name string, metadata *model.BootstrapNodeMetadata) string { 403 if name != "" && name != "istio-proxy" { 404 return name 405 } 406 if app, ok := metadata.Labels["app"]; ok { 407 return app + "." + metadata.Namespace 408 } 409 if metadata.WorkloadName != "" { 410 return metadata.WorkloadName + "." + metadata.Namespace 411 } 412 if metadata.Namespace != "" { 413 return "istio-proxy." + metadata.Namespace 414 } 415 return "istio-proxy" 416 } 417 418 func getProxyConfigOptions(metadata *model.BootstrapNodeMetadata) ([]option.Instance, error) { 419 config := metadata.ProxyConfig 420 421 // Add a few misc options. 422 opts := make([]option.Instance, 0) 423 424 opts = append(opts, option.ProxyConfig(config), 425 option.Cluster(getServiceCluster(metadata)), 426 option.PilotGRPCAddress(config.DiscoveryAddress), 427 option.DiscoveryAddress(config.DiscoveryAddress), 428 option.StatsdAddress(config.StatsdUdpAddress), 429 option.XDSRootCert(metadata.XDSRootCert)) 430 431 // Add tracing options. 432 if config.Tracing != nil { 433 isH2 := false 434 switch tracer := config.Tracing.Tracer.(type) { 435 case *meshAPI.Tracing_Zipkin_: 436 opts = append(opts, option.ZipkinAddress(tracer.Zipkin.Address)) 437 case *meshAPI.Tracing_Lightstep_: 438 isH2 = true 439 // Write the token file. 440 lightstepAccessTokenPath := lightstepAccessTokenFile(config.ConfigPath) 441 //nolint: staticcheck // Lightstep deprecated 442 err := os.WriteFile(lightstepAccessTokenPath, []byte(tracer.Lightstep.AccessToken), 0o666) 443 if err != nil { 444 return nil, err 445 } 446 opts = append(opts, option.LightstepAddress(tracer.Lightstep.Address), 447 option.LightstepToken(lightstepAccessTokenPath)) 448 case *meshAPI.Tracing_Datadog_: 449 opts = append(opts, option.DataDogAddress(tracer.Datadog.Address)) 450 case *meshAPI.Tracing_Stackdriver_: 451 projectID, projFound := metadata.PlatformMetadata[platform.GCPProject] 452 if !projFound { 453 return nil, errors.New("unable to process Stackdriver tracer: missing GCP Project") 454 } 455 456 opts = append(opts, option.StackDriverEnabled(true), 457 option.StackDriverProjectID(projectID), 458 option.StackDriverDebug(tracer.Stackdriver.Debug), 459 option.StackDriverMaxAnnotations(getInt64ValueOrDefault(tracer.Stackdriver.MaxNumberOfAnnotations, 200)), 460 option.StackDriverMaxAttributes(getInt64ValueOrDefault(tracer.Stackdriver.MaxNumberOfAttributes, 200)), 461 option.StackDriverMaxEvents(getInt64ValueOrDefault(tracer.Stackdriver.MaxNumberOfMessageEvents, 200))) 462 case *meshAPI.Tracing_OpenCensusAgent_: 463 c := tracer.OpenCensusAgent.Context 464 opts = append(opts, option.OpenCensusAgentAddress(tracer.OpenCensusAgent.Address), 465 option.OpenCensusAgentContexts(c)) 466 } 467 468 opts = append(opts, option.TracingTLS(config.Tracing.TlsSettings, metadata, isH2)) 469 } 470 471 // Add options for Envoy metrics. 472 if config.EnvoyMetricsService != nil && config.EnvoyMetricsService.Address != "" { 473 opts = append(opts, option.EnvoyMetricsServiceAddress(config.EnvoyMetricsService.Address), 474 option.EnvoyMetricsServiceTLS(config.EnvoyMetricsService.TlsSettings, metadata), 475 option.EnvoyMetricsServiceTCPKeepalive(config.EnvoyMetricsService.TcpKeepalive)) 476 } else if config.EnvoyMetricsServiceAddress != "" { // nolint: staticcheck 477 opts = append(opts, option.EnvoyMetricsServiceAddress(config.EnvoyMetricsService.Address)) 478 } 479 480 // Add options for Envoy access log. 481 if config.EnvoyAccessLogService != nil && config.EnvoyAccessLogService.Address != "" { 482 opts = append(opts, option.EnvoyAccessLogServiceAddress(config.EnvoyAccessLogService.Address), 483 option.EnvoyAccessLogServiceTLS(config.EnvoyAccessLogService.TlsSettings, metadata), 484 option.EnvoyAccessLogServiceTCPKeepalive(config.EnvoyAccessLogService.TcpKeepalive)) 485 } 486 487 return opts, nil 488 } 489 490 func getInt64ValueOrDefault(src *wrapperspb.Int64Value, defaultVal int64) int64 { 491 val := defaultVal 492 if src != nil { 493 val = src.Value 494 } 495 return val 496 } 497 498 type setMetaFunc func(m map[string]any, key string, val string) 499 500 func extractMetadata(envs []string, prefix string, set setMetaFunc, meta map[string]any) { 501 metaPrefixLen := len(prefix) 502 for _, e := range envs { 503 if !shouldExtract(e, prefix) { 504 continue 505 } 506 v := e[metaPrefixLen:] 507 if !isEnvVar(v) { 508 continue 509 } 510 metaKey, metaVal := parseEnvVar(v) 511 set(meta, metaKey, metaVal) 512 } 513 } 514 515 func shouldExtract(envVar, prefix string) bool { 516 return strings.HasPrefix(envVar, prefix) 517 } 518 519 func isEnvVar(str string) bool { 520 return strings.Contains(str, "=") 521 } 522 523 func parseEnvVar(varStr string) (string, string) { 524 parts := strings.SplitN(varStr, "=", 2) 525 if len(parts) != 2 { 526 return varStr, "" 527 } 528 return parts[0], parts[1] 529 } 530 531 func jsonStringToMap(jsonStr string) (m map[string]string) { 532 err := json.Unmarshal([]byte(jsonStr), &m) 533 if err != nil { 534 log.Warnf("Env variable with value %q failed json unmarshal: %v", jsonStr, err) 535 } 536 return 537 } 538 539 func extractAttributesMetadata(envVars []string, plat platform.Environment, meta *model.BootstrapNodeMetadata) { 540 for _, varStr := range envVars { 541 name, val := parseEnvVar(varStr) 542 switch name { 543 case "ISTIO_METAJSON_LABELS": 544 m := jsonStringToMap(val) 545 if len(m) > 0 { 546 meta.Labels = m 547 meta.StaticLabels = m 548 } 549 case "POD_NAME": 550 meta.InstanceName = val 551 case "POD_NAMESPACE": 552 meta.Namespace = val 553 case "SERVICE_ACCOUNT": 554 meta.ServiceAccount = val 555 } 556 } 557 if plat != nil && len(plat.Metadata()) > 0 { 558 meta.PlatformMetadata = plat.Metadata() 559 } 560 } 561 562 // MetadataOptions for constructing node metadata. 563 type MetadataOptions struct { 564 Envs []string 565 Platform platform.Environment 566 InstanceIPs []string 567 StsPort int 568 ID string 569 ProxyConfig *meshAPI.ProxyConfig 570 PilotSubjectAltName []string 571 CredentialSocketExists bool 572 XDSRootCert string 573 OutlierLogPath string 574 annotationFilePath string 575 EnvoyStatusPort int 576 EnvoyPrometheusPort int 577 ExitOnZeroActiveConnections bool 578 MetadataDiscovery bool 579 } 580 581 const ( 582 // DefaultDeploymentUniqueLabelKey is the default key of the selector that is added 583 // to existing ReplicaSets (and label key that is added to its pods) to prevent the existing ReplicaSets 584 // to select new pods (and old pods being select by new ReplicaSet). 585 DefaultDeploymentUniqueLabelKey string = "pod-template-hash" 586 ) 587 588 // GetNodeMetaData function uses an environment variable contract 589 // ISTIO_METAJSON_* env variables contain json_string in the value. 590 // The name of variable is ignored. 591 // ISTIO_META_* env variables are passed through 592 func GetNodeMetaData(options MetadataOptions) (*model.Node, error) { 593 meta := &model.BootstrapNodeMetadata{} 594 untypedMeta := map[string]any{} 595 596 for k, v := range options.ProxyConfig.GetProxyMetadata() { 597 if strings.HasPrefix(k, IstioMetaPrefix) { 598 untypedMeta[strings.TrimPrefix(k, IstioMetaPrefix)] = v 599 } 600 } 601 602 extractMetadata(options.Envs, IstioMetaPrefix, func(m map[string]any, key string, val string) { 603 m[key] = val 604 }, untypedMeta) 605 606 extractMetadata(options.Envs, IstioMetaJSONPrefix, func(m map[string]any, key string, val string) { 607 err := json.Unmarshal([]byte(val), &m) 608 if err != nil { 609 log.Warnf("Env variable %s [%s] failed json unmarshal: %v", key, val, err) 610 } 611 }, untypedMeta) 612 613 j, err := json.Marshal(untypedMeta) 614 if err != nil { 615 return nil, err 616 } 617 618 if err := json.Unmarshal(j, meta); err != nil { 619 return nil, err 620 } 621 622 meta = SetIstioVersion(meta) 623 624 // Support multiple network interfaces, removing duplicates. 625 meta.InstanceIPs = removeDuplicates(options.InstanceIPs) 626 627 // Add STS port into node metadata if it is not 0. This is read by envoy telemetry filters 628 if options.StsPort != 0 { 629 meta.StsPort = strconv.Itoa(options.StsPort) 630 } 631 meta.EnvoyStatusPort = options.EnvoyStatusPort 632 meta.EnvoyPrometheusPort = options.EnvoyPrometheusPort 633 meta.ExitOnZeroActiveConnections = model.StringBool(options.ExitOnZeroActiveConnections) 634 meta.MetadataDiscovery = model.StringBool(options.MetadataDiscovery) 635 636 meta.ProxyConfig = (*model.NodeMetaProxyConfig)(options.ProxyConfig) 637 638 extractAttributesMetadata(options.Envs, options.Platform, meta) 639 // Add all instance labels with lower precedence than pod labels 640 extractInstanceLabels(options.Platform, meta) 641 642 // Add all pod labels found from filesystem 643 // These are typically volume mounted by the downward API 644 lbls, err := readPodLabels() 645 if err == nil { 646 meta.Labels = map[string]string{} 647 for k, v := range meta.StaticLabels { 648 meta.Labels[k] = v 649 } 650 for k, v := range lbls { 651 // ignore `pod-template-hash` label 652 if k == DefaultDeploymentUniqueLabelKey { 653 continue 654 } 655 meta.Labels[k] = v 656 } 657 } else { 658 if os.IsNotExist(err) { 659 log.Debugf("failed to read pod labels: %v", err) 660 } else { 661 log.Warnf("failed to read pod labels: %v", err) 662 } 663 } 664 665 // Add all pod annotations found from filesystem 666 // These are typically volume mounted by the downward API 667 annos, err := ReadPodAnnotations(options.annotationFilePath) 668 if err == nil { 669 if meta.Annotations == nil { 670 meta.Annotations = map[string]string{} 671 } 672 for k, v := range annos { 673 meta.Annotations[k] = v 674 } 675 } else { 676 if os.IsNotExist(err) { 677 log.Debugf("failed to read pod annotations: %v", err) 678 } else { 679 log.Warnf("failed to read pod annotations: %v", err) 680 } 681 } 682 683 var l *core.Locality 684 if meta.Labels[model.LocalityLabel] == "" && options.Platform != nil { 685 // The locality string was not set, try to get locality from platform 686 l = options.Platform.Locality() 687 } else { 688 // replace "." with "/" 689 localityString := model.GetLocalityLabel(meta.Labels[model.LocalityLabel]) 690 if localityString != "" { 691 // override the label with the sanitized value 692 meta.Labels[model.LocalityLabel] = localityString 693 } 694 l = model.ConvertLocality(localityString) 695 } 696 697 meta.PilotSubjectAltName = options.PilotSubjectAltName 698 meta.XDSRootCert = options.XDSRootCert 699 meta.OutlierLogPath = options.OutlierLogPath 700 if options.CredentialSocketExists { 701 untypedMeta[security.CredentialMetaDataName] = "true" 702 } 703 704 return &model.Node{ 705 ID: options.ID, 706 Metadata: meta, 707 RawMetadata: untypedMeta, 708 Locality: l, 709 }, nil 710 } 711 712 func SetIstioVersion(meta *model.BootstrapNodeMetadata) *model.BootstrapNodeMetadata { 713 if meta.IstioVersion == "" { 714 meta.IstioVersion = version.Info.Version 715 } 716 return meta 717 } 718 719 // ConvertNodeToXDSNode creates an Envoy node descriptor from Istio node descriptor. 720 func ConvertNodeToXDSNode(node *model.Node) *core.Node { 721 // First pass translates typed metadata 722 js, err := json.Marshal(node.Metadata) 723 if err != nil { 724 log.Warnf("Failed to marshal node metadata to JSON %#v: %v", node.Metadata, err) 725 } 726 pbst := &structpb.Struct{} 727 if err = protomarshal.Unmarshal(js, pbst); err != nil { 728 log.Warnf("Failed to unmarshal node metadata from JSON %#v: %v", node.Metadata, err) 729 } 730 // Second pass translates untyped metadata for "unknown" fields 731 for k, v := range node.RawMetadata { 732 if _, f := pbst.Fields[k]; !f { 733 fjs, err := json.Marshal(v) 734 if err != nil { 735 log.Warnf("Failed to marshal field metadata to JSON %#v: %v", k, err) 736 } 737 pbv := &structpb.Value{} 738 if err = protomarshal.Unmarshal(fjs, pbv); err != nil { 739 log.Warnf("Failed to unmarshal field metadata from JSON %#v: %v", k, err) 740 } 741 pbst.Fields[k] = pbv 742 } 743 } 744 return &core.Node{ 745 Id: node.ID, 746 Cluster: getServiceCluster(node.Metadata), 747 Locality: node.Locality, 748 Metadata: pbst, 749 } 750 } 751 752 // ConvertXDSNodeToNode parses Istio node descriptor from an Envoy node descriptor, using only typed metadata. 753 func ConvertXDSNodeToNode(node *core.Node) *model.Node { 754 b, err := protomarshal.MarshalProtoNames(node.Metadata) 755 if err != nil { 756 log.Warnf("Failed to marshal node metadata to JSON %q: %v", node.Metadata, err) 757 } 758 metadata := &model.BootstrapNodeMetadata{} 759 err = json.Unmarshal(b, metadata) 760 if err != nil { 761 log.Warnf("Failed to unmarshal node metadata from JSON %q: %v", node.Metadata, err) 762 } 763 if metadata.ProxyConfig == nil { 764 metadata.ProxyConfig = &model.NodeMetaProxyConfig{} 765 metadata.ProxyConfig.ClusterName = &meshAPI.ProxyConfig_ServiceCluster{ServiceCluster: node.Cluster} 766 } 767 768 return &model.Node{ 769 ID: node.Id, 770 Locality: node.Locality, 771 Metadata: metadata, 772 } 773 } 774 775 // Extracts instance labels for the platform into model.NodeMetadata.Labels 776 // only if not running on Kubernetes 777 func extractInstanceLabels(plat platform.Environment, meta *model.BootstrapNodeMetadata) { 778 if plat == nil || meta == nil || plat.IsKubernetes() { 779 return 780 } 781 instanceLabels := plat.Labels() 782 if meta.StaticLabels == nil { 783 meta.StaticLabels = map[string]string{} 784 } 785 for k, v := range instanceLabels { 786 meta.StaticLabels[k] = v 787 } 788 } 789 790 func readPodLabels() (map[string]string, error) { 791 b, err := os.ReadFile(constants.PodInfoLabelsPath) 792 if err != nil { 793 return nil, err 794 } 795 return ParseDownwardAPI(string(b)) 796 } 797 798 func ReadPodAnnotations(path string) (map[string]string, error) { 799 if path == "" { 800 path = constants.PodInfoAnnotationsPath 801 } 802 b, err := os.ReadFile(path) 803 if err != nil { 804 return nil, err 805 } 806 return ParseDownwardAPI(string(b)) 807 } 808 809 // ParseDownwardAPI parses fields which are stored as format `%s=%q` back to a map 810 func ParseDownwardAPI(i string) (map[string]string, error) { 811 res := map[string]string{} 812 for _, line := range strings.Split(i, "\n") { 813 sl := strings.SplitN(line, "=", 2) 814 if len(sl) != 2 { 815 continue 816 } 817 key := sl[0] 818 // Strip the leading/trailing quotes 819 val, err := strconv.Unquote(sl[1]) 820 if err != nil { 821 return nil, fmt.Errorf("failed to unquote %v: %v", sl[1], err) 822 } 823 res[key] = val 824 } 825 return res, nil 826 } 827 828 func removeDuplicates(values []string) []string { 829 set := sets.New[string]() 830 newValues := make([]string, 0, len(values)) 831 for _, v := range values { 832 if !set.InsertContains(v) { 833 newValues = append(newValues, v) 834 } 835 } 836 return newValues 837 }