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  }