1// Copyright 2018 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.
14package bootstrap
15
16import (
17	"encoding/json"
18	"fmt"
19	"io/ioutil"
20	"net/http"
21	"net/http/httptest"
22	"net/url"
23	"os"
24	"path"
25	"reflect"
26	"regexp"
27	"strings"
28	"testing"
29
30	v1 "github.com/census-instrumentation/opencensus-proto/gen-go/trace/v1"
31	core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
32	v2 "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v2"
33	tracev2 "github.com/envoyproxy/go-control-plane/envoy/config/trace/v2"
34	matcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher"
35	"github.com/ghodss/yaml"
36	"github.com/gogo/protobuf/proto"
37	"github.com/golang/protobuf/jsonpb"
38	"github.com/golang/protobuf/ptypes"
39	diff "gopkg.in/d4l3k/messagediff.v1"
40
41	"istio.io/api/annotation"
42	meshconfig "istio.io/api/mesh/v1alpha1"
43
44	"istio.io/istio/pilot/test/util"
45	"istio.io/istio/pkg/bootstrap/platform"
46)
47
48type stats struct {
49	prefixes string
50	suffixes string
51	regexps  string
52}
53
54var (
55	// The following set of inclusions add minimal upstream and downstream metrics.
56	// Upstream metrics record client side measurements.
57	// Downstream metrics record server side measurements.
58	upstreamStatsSuffixes = "upstream_rq_1xx,upstream_rq_2xx,upstream_rq_3xx,upstream_rq_4xx,upstream_rq_5xx," +
59		"upstream_rq_time,upstream_cx_tx_bytes_total,upstream_cx_rx_bytes_total,upstream_cx_total"
60
61	// example downstream metric: http.10.16.48.230_8080.downstream_rq_2xx
62	// http.<pod_ip>_<port>.downstream_rq_2xx
63	// This metric is collected at the inbound listener at a sidecar.
64	// All the other downstream metrics at a sidecar are from the application to the local sidecar.
65	downstreamStatsSuffixes = "downstream_rq_1xx,downstream_rq_2xx,downstream_rq_3xx,downstream_rq_4xx,downstream_rq_5xx," +
66		"downstream_rq_time,downstream_cx_tx_bytes_total,downstream_cx_rx_bytes_total,downstream_cx_total"
67)
68
69// Generate configs for the default configs used by istio.
70// If the template is updated, copy the new golden files from out:
71// cp $TOP/out/linux_amd64/release/bootstrap/all/envoy-rev0.json pkg/bootstrap/testdata/all_golden.json
72// cp $TOP/out/linux_amd64/release/bootstrap/auth/envoy-rev0.json pkg/bootstrap/testdata/auth_golden.json
73// cp $TOP/out/linux_amd64/release/bootstrap/default/envoy-rev0.json pkg/bootstrap/testdata/default_golden.json
74// cp $TOP/out/linux_amd64/release/bootstrap/tracing_datadog/envoy-rev0.json pkg/bootstrap/testdata/tracing_datadog_golden.json
75// cp $TOP/out/linux_amd64/release/bootstrap/tracing_lightstep/envoy-rev0.json pkg/bootstrap/testdata/tracing_lightstep_golden.json
76// cp $TOP/out/linux_amd64/release/bootstrap/tracing_zipkin/envoy-rev0.json pkg/bootstrap/testdata/tracing_zipkin_golden.json
77func TestGolden(t *testing.T) {
78	out := "/tmp"
79	var ts *httptest.Server
80
81	cases := []struct {
82		base                       string
83		envVars                    map[string]string
84		annotations                map[string]string
85		sdsUDSPath                 string
86		sdsTokenPath               string
87		expectLightstepAccessToken bool
88		stats                      stats
89		checkLocality              bool
90		setup                      func()
91		teardown                   func()
92		check                      func(got *v2.Bootstrap, t *testing.T)
93	}{
94		{
95			base: "auth",
96		},
97		{
98			base:         "authsds",
99			sdsUDSPath:   "udspath",
100			sdsTokenPath: "/var/run/secrets/tokens/istio-token",
101		},
102		{
103			base: "default",
104		},
105		{
106			base: "running",
107			envVars: map[string]string{
108				"ISTIO_META_ISTIO_PROXY_SHA":   "istio-proxy:sha",
109				"ISTIO_META_INTERCEPTION_MODE": "REDIRECT",
110				"ISTIO_META_ISTIO_VERSION":     "release-3.1",
111				"ISTIO_META_POD_NAME":          "svc-0-0-0-6944fb884d-4pgx8",
112				"POD_NAME":                     "svc-0-0-0-6944fb884d-4pgx8",
113				"POD_NAMESPACE":                "test",
114				"INSTANCE_IP":                  "10.10.10.1",
115				"ISTIO_METAJSON_LABELS":        `{"version": "v1alpha1", "app": "test", "istio-locality":"regionA.zoneB.sub_zoneC"}`,
116			},
117			annotations: map[string]string{
118				"istio.io/insecurepath": "{\"paths\":[\"/metrics\",\"/live\"]}",
119			},
120			checkLocality: true,
121		},
122		{
123			base: "runningsds",
124			envVars: map[string]string{
125				"ISTIO_META_ISTIO_PROXY_SHA":   "istio-proxy:sha",
126				"ISTIO_META_INTERCEPTION_MODE": "REDIRECT",
127				"ISTIO_META_ISTIO_VERSION":     "release-3.1",
128				"ISTIO_META_POD_NAME":          "svc-0-0-0-6944fb884d-4pgx8",
129				"POD_NAME":                     "svc-0-0-0-6944fb884d-4pgx8",
130				"POD_NAMESPACE":                "test",
131				"INSTANCE_IP":                  "10.10.10.1",
132				"ISTIO_METAJSON_LABELS":        `{"version": "v1alpha1", "app": "test", "istio-locality":"regionA.zoneB.sub_zoneC"}`,
133			},
134			annotations: map[string]string{
135				"istio.io/insecurepath": "{\"paths\":[\"/metrics\",\"/live\"]}",
136			},
137			sdsUDSPath:    "udspath",
138			sdsTokenPath:  "/var/run/secrets/tokens/istio-token",
139			checkLocality: true,
140		},
141		{
142			base:                       "tracing_lightstep",
143			expectLightstepAccessToken: true,
144		},
145		{
146			base: "tracing_zipkin",
147		},
148		{
149			base: "tracing_datadog",
150		},
151		{
152			base: "tracing_stackdriver",
153			setup: func() {
154				ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
155					fmt.Fprintln(w, "my-sd-project")
156				}))
157
158				u, err := url.Parse(ts.URL)
159				if err != nil {
160					t.Fatalf("Unable to parse mock server url: %v", err)
161				}
162				_ = os.Setenv("GCE_METADATA_HOST", u.Host)
163			},
164			teardown: func() {
165				if ts != nil {
166					ts.Close()
167				}
168				_ = os.Unsetenv("GCE_METADATA_HOST")
169			},
170			check: func(got *v2.Bootstrap, t *testing.T) {
171				// nolint: staticcheck
172				cfg := got.Tracing.Http.GetTypedConfig()
173				sdMsg := tracev2.OpenCensusConfig{}
174				if err := ptypes.UnmarshalAny(cfg, &sdMsg); err != nil {
175					t.Fatalf("unable to parse: %v %v", cfg, err)
176				}
177
178				want := tracev2.OpenCensusConfig{
179					TraceConfig: &v1.TraceConfig{
180						Sampler: &v1.TraceConfig_ConstantSampler{
181							ConstantSampler: &v1.ConstantSampler{
182								Decision: v1.ConstantSampler_ALWAYS_PARENT,
183							},
184						},
185						MaxNumberOfAttributes:    200,
186						MaxNumberOfAnnotations:   201,
187						MaxNumberOfMessageEvents: 201,
188						MaxNumberOfLinks:         200,
189					},
190					StackdriverExporterEnabled: true,
191					StdoutExporterEnabled:      true,
192					StackdriverProjectId:       "my-sd-project",
193					IncomingTraceContext: []tracev2.OpenCensusConfig_TraceContext{
194						tracev2.OpenCensusConfig_CLOUD_TRACE_CONTEXT,
195						tracev2.OpenCensusConfig_TRACE_CONTEXT,
196						tracev2.OpenCensusConfig_GRPC_TRACE_BIN,
197						tracev2.OpenCensusConfig_B3},
198					OutgoingTraceContext: []tracev2.OpenCensusConfig_TraceContext{
199						tracev2.OpenCensusConfig_CLOUD_TRACE_CONTEXT,
200						tracev2.OpenCensusConfig_TRACE_CONTEXT,
201						tracev2.OpenCensusConfig_GRPC_TRACE_BIN,
202						tracev2.OpenCensusConfig_B3},
203				}
204
205				p, equal := diff.PrettyDiff(sdMsg, want)
206				if !equal {
207					t.Fatalf("t diff: %v\ngot: %v\nwant: %v\n", p, sdMsg, want)
208				}
209			},
210		},
211		{
212			// Specify zipkin/statsd address, similar with the default config in v1 tests
213			base: "all",
214		},
215		{
216			base: "stats_inclusion",
217			annotations: map[string]string{
218				"sidecar.istio.io/statsInclusionPrefixes": "prefix1,prefix2",
219				"sidecar.istio.io/statsInclusionSuffixes": "suffix1,suffix2",
220				"sidecar.istio.io/extraStatTags":          "dlp_status,dlp_error",
221			},
222			stats: stats{prefixes: "prefix1,prefix2",
223				suffixes: "suffix1,suffix2"},
224		},
225		{
226			base: "stats_inclusion",
227			annotations: map[string]string{
228				"sidecar.istio.io/statsInclusionSuffixes": upstreamStatsSuffixes + "," + downstreamStatsSuffixes,
229				"sidecar.istio.io/extraStatTags":          "dlp_status,dlp_error",
230			},
231			stats: stats{
232				suffixes: upstreamStatsSuffixes + "," + downstreamStatsSuffixes},
233		},
234		{
235			base: "stats_inclusion",
236			annotations: map[string]string{
237				"sidecar.istio.io/statsInclusionPrefixes": "http.{pod_ip}_",
238				"sidecar.istio.io/extraStatTags":          "dlp_status,dlp_error",
239			},
240			// {pod_ip} is unrolled
241			stats: stats{prefixes: "http.10.3.3.3_,http.10.4.4.4_,http.10.5.5.5_,http.10.6.6.6_"},
242		},
243		{
244			base: "stats_inclusion",
245			annotations: map[string]string{
246				"sidecar.istio.io/statsInclusionRegexps": "http.[0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*_8080.downstream_rq_time",
247				"sidecar.istio.io/extraStatTags":         "dlp_status,dlp_error",
248			},
249			stats: stats{regexps: "http.[0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*_8080.downstream_rq_time"},
250		},
251		{
252			base: "tracing_tls",
253		},
254	}
255
256	for _, c := range cases {
257		t.Run("Bootstrap-"+c.base, func(t *testing.T) {
258			if c.setup != nil {
259				c.setup()
260			}
261			if c.teardown != nil {
262				defer c.teardown()
263			}
264
265			proxyConfig, err := loadProxyConfig(c.base, out, t)
266			if err != nil {
267				t.Fatalf("unable to load proxy config: %s\n%v", c.base, err)
268			}
269
270			_, localEnv := createEnv(t, map[string]string{}, c.annotations)
271			for k, v := range c.envVars {
272				localEnv = append(localEnv, k+"="+v)
273			}
274
275			fn, err := New(Config{
276				Node:    "sidecar~1.2.3.4~foo~bar",
277				Proxy:   proxyConfig,
278				PlatEnv: &fakePlatform{},
279				PilotSubjectAltName: []string{
280					"spiffe://cluster.local/ns/istio-system/sa/istio-pilot-service-account"},
281				LocalEnv:          localEnv,
282				NodeIPs:           []string{"10.3.3.3", "10.4.4.4", "10.5.5.5", "10.6.6.6", "10.4.4.4"},
283				OutlierLogPath:    "/dev/stdout",
284				PilotCertProvider: "istiod",
285			}).CreateFileForEpoch(0)
286			if err != nil {
287				t.Fatal(err)
288			}
289
290			read, err := ioutil.ReadFile(fn)
291			if err != nil {
292				t.Error("Error reading generated file ", err)
293				return
294			}
295
296			// apply minor modifications for the generated file so that tests are consistent
297			// across different env setups
298			err = ioutil.WriteFile(fn, correctForEnvDifference(read, !c.checkLocality), 0700)
299			if err != nil {
300				t.Error("Error modifying generated file ", err)
301				return
302			}
303
304			// re-read generated file with the changes having been made
305			read, err = ioutil.ReadFile(fn)
306			if err != nil {
307				t.Error("Error reading generated file ", err)
308				return
309			}
310
311			goldenFile := "testdata/" + c.base + "_golden.json"
312			util.RefreshGoldenFile(read, goldenFile, t)
313
314			golden, err := ioutil.ReadFile(goldenFile)
315			if err != nil {
316				golden = []byte{}
317			}
318
319			realM := v2.Bootstrap{}
320			goldenM := v2.Bootstrap{}
321
322			jgolden, err := yaml.YAMLToJSON(golden)
323
324			if err != nil {
325				t.Fatalf("unable to convert: %s %v", c.base, err)
326			}
327
328			if err = jsonpb.UnmarshalString(string(jgolden), &goldenM); err != nil {
329				t.Fatalf("invalid json %s %s\n%v", c.base, err, string(jgolden))
330			}
331
332			if err = goldenM.Validate(); err != nil {
333				t.Fatalf("invalid golden %s: %v", c.base, err)
334			}
335
336			jreal, err := yaml.YAMLToJSON(read)
337
338			if err != nil {
339				t.Fatalf("unable to convert: %s (%s) %v", c.base, fn, err)
340			}
341
342			if err = jsonpb.UnmarshalString(string(jreal), &realM); err != nil {
343				t.Fatalf("invalid json %v\n%s", err, string(read))
344			}
345
346			if err = realM.Validate(); err != nil {
347				t.Fatalf("invalid generated file %s: %v", c.base, err)
348			}
349
350			checkStatsMatcher(t, &realM, &goldenM, c.stats)
351
352			if c.check != nil {
353				c.check(&realM, t)
354			}
355
356			checkOpencensusConfig(t, &realM, &goldenM)
357
358			if !reflect.DeepEqual(realM, goldenM) {
359				s, _ := diff.PrettyDiff(goldenM, realM)
360				t.Logf("difference: %s", s)
361				t.Fatalf("\n got: %v\nwant: %v", realM, goldenM)
362			}
363
364			// Check if the LightStep access token file exists
365			_, err = os.Stat(lightstepAccessTokenFile(path.Dir(fn)))
366			if c.expectLightstepAccessToken {
367				if os.IsNotExist(err) {
368					t.Error("expected to find a LightStep access token file but none found")
369				} else if err != nil {
370					t.Error("error running Stat on file: ", err)
371				}
372			} else {
373				if err == nil {
374					t.Error("found a LightStep access token file but none was expected")
375				} else if !os.IsNotExist(err) {
376					t.Error("error running Stat on file: ", err)
377				}
378			}
379		})
380	}
381}
382
383func checkListStringMatcher(t *testing.T, got *matcher.ListStringMatcher, want string, typ string) {
384	var patterns []string
385	for _, pattern := range got.GetPatterns() {
386		var pat string
387		switch typ {
388		case "prefix":
389			pat = pattern.GetPrefix()
390		case "suffix":
391			pat = pattern.GetSuffix()
392		case "regexp":
393			// Migration tracked in https://github.com/istio/istio/issues/17127
394			//nolint: staticcheck
395			pat = pattern.GetRegex()
396		}
397
398		if pat != "" {
399			patterns = append(patterns, pat)
400		}
401	}
402	gotPattern := strings.Join(patterns, ",")
403	if want != gotPattern {
404		t.Fatalf("%s mismatch:\ngot: %s\nwant: %s", typ, gotPattern, want)
405	}
406}
407
408func checkOpencensusConfig(t *testing.T, got, want *v2.Bootstrap) {
409	if want.Tracing == nil {
410		return
411	}
412
413	if want.Tracing.Http.Name != "envoy.tracers.opencensus" {
414		return
415	}
416
417	if !reflect.DeepEqual(got.Tracing.Http, want.Tracing.Http) {
418		p, _ := diff.PrettyDiff(got.Tracing.Http, want.Tracing.Http)
419		t.Fatalf("t diff: %v\ngot:\n %v\nwant:\n %v\n", p, got.Tracing.Http, want.Tracing.Http)
420	}
421}
422
423func checkStatsMatcher(t *testing.T, got, want *v2.Bootstrap, stats stats) {
424	gsm := got.GetStatsConfig().GetStatsMatcher()
425
426	if stats.prefixes == "" {
427		stats.prefixes = v2Prefixes + requiredEnvoyStatsMatcherInclusionPrefixes + v2Suffix
428	} else {
429		stats.prefixes = v2Prefixes + stats.prefixes + "," + requiredEnvoyStatsMatcherInclusionPrefixes + v2Suffix
430	}
431
432	if err := gsm.Validate(); err != nil {
433		t.Fatalf("Generated invalid matcher: %v", err)
434	}
435
436	checkListStringMatcher(t, gsm.GetInclusionList(), stats.prefixes, "prefix")
437	checkListStringMatcher(t, gsm.GetInclusionList(), stats.suffixes, "suffix")
438	checkListStringMatcher(t, gsm.GetInclusionList(), stats.regexps, "regexp")
439
440	// remove StatsMatcher for general matching
441	got.StatsConfig.StatsMatcher = nil
442	want.StatsConfig.StatsMatcher = nil
443
444	// remove StatsMatcher metadata from matching
445	delete(got.Node.Metadata.Fields, annotation.SidecarStatsInclusionPrefixes.Name)
446	delete(want.Node.Metadata.Fields, annotation.SidecarStatsInclusionPrefixes.Name)
447	delete(got.Node.Metadata.Fields, annotation.SidecarStatsInclusionSuffixes.Name)
448	delete(want.Node.Metadata.Fields, annotation.SidecarStatsInclusionSuffixes.Name)
449	delete(got.Node.Metadata.Fields, annotation.SidecarStatsInclusionRegexps.Name)
450	delete(want.Node.Metadata.Fields, annotation.SidecarStatsInclusionRegexps.Name)
451}
452
453type regexReplacement struct {
454	pattern     *regexp.Regexp
455	replacement []byte
456}
457
458// correctForEnvDifference corrects the portions of a generated bootstrap config that vary depending on the environment
459// so that they match the golden file's expected value.
460func correctForEnvDifference(in []byte, excludeLocality bool) []byte {
461	replacements := []regexReplacement{
462		// Lightstep access tokens are written to a file and that path is dependent upon the environment variables that
463		// are set. Standardize the path so that golden files can be properly checked.
464		{
465			pattern:     regexp.MustCompile(`("access_token_file": ").*(lightstep_access_token.txt")`),
466			replacement: []byte("$1/test-path/$2"),
467		},
468		{
469			// Example: "customConfigFile":"../../tools/packaging/common/envoy_bootstrap_v2.json"
470			// The path may change in CI/other machines
471			pattern:     regexp.MustCompile(`("customConfigFile":").*(envoy_bootstrap_v2.json")`),
472			replacement: []byte(`"customConfigFile":"envoy_bootstrap_v2.json"`),
473		},
474	}
475	if excludeLocality {
476		// zone and region can vary based on the environment, so it shouldn't be considered in the diff.
477		replacements = append(replacements,
478			regexReplacement{
479				pattern:     regexp.MustCompile(`"zone": ".+"`),
480				replacement: []byte("\"zone\": \"\""),
481			},
482			regexReplacement{
483				pattern:     regexp.MustCompile(`"region": ".+"`),
484				replacement: []byte("\"region\": \"\""),
485			})
486	}
487
488	out := in
489	for _, r := range replacements {
490		out = r.pattern.ReplaceAll(out, r.replacement)
491	}
492	return out
493}
494
495func loadProxyConfig(base, out string, _ *testing.T) (*meshconfig.ProxyConfig, error) {
496	content, err := ioutil.ReadFile("testdata/" + base + ".proxycfg")
497	if err != nil {
498		return nil, err
499	}
500	cfg := &meshconfig.ProxyConfig{}
501	err = proto.UnmarshalText(string(content), cfg)
502	if err != nil {
503		return nil, err
504	}
505
506	// Exported from makefile or env
507	cfg.ConfigPath = out + "/bootstrap/" + base
508	gobase := os.Getenv("ISTIO_GO")
509	if gobase == "" {
510		gobase = "../.."
511	}
512	cfg.CustomConfigFile = gobase + "/tools/packaging/common/envoy_bootstrap_v2.json"
513	return cfg, nil
514}
515
516func TestIsIPv6Proxy(t *testing.T) {
517	tests := []struct {
518		name     string
519		addrs    []string
520		expected bool
521	}{
522		{
523			name:     "ipv4 only",
524			addrs:    []string{"1.1.1.1", "127.0.0.1", "2.2.2.2"},
525			expected: false,
526		},
527		{
528			name:     "ipv6 only",
529			addrs:    []string{"1111:2222::1", "::1", "2222:3333::1"},
530			expected: true,
531		},
532		{
533			name:     "mixed ipv4 and ipv6",
534			addrs:    []string{"1111:2222::1", "::1", "127.0.0.1", "2.2.2.2", "2222:3333::1"},
535			expected: false,
536		},
537	}
538	for _, tt := range tests {
539		result := isIPv6Proxy(tt.addrs)
540		if result != tt.expected {
541			t.Errorf("Test %s failed, expected: %t got: %t", tt.name, tt.expected, result)
542		}
543	}
544}
545
546// createEnv takes labels and annotations are returns environment in go format.
547func createEnv(t *testing.T, labels map[string]string, anno map[string]string) (map[string]string, []string) {
548	merged := map[string]string{}
549	mergeMap(merged, labels)
550	mergeMap(merged, anno)
551
552	envs := make([]string, 0)
553
554	if labels != nil {
555		envs = append(envs, encodeAsJSON(t, labels, "LABELS"))
556	}
557
558	if anno != nil {
559		envs = append(envs, encodeAsJSON(t, anno, "ANNOTATIONS"))
560	}
561	return merged, envs
562}
563
564func encodeAsJSON(t *testing.T, data map[string]string, name string) string {
565	jsonStr, err := json.Marshal(data)
566	if err != nil {
567		t.Fatalf("failed to marshal %s %v: %v", name, data, err)
568	}
569	return IstioMetaJSONPrefix + name + "=" + string(jsonStr)
570}
571
572func TestNodeMetadataEncodeEnvWithIstioMetaPrefix(t *testing.T) {
573	originalKey := "foo"
574	notIstioMetaKey := "NOT_AN_" + IstioMetaPrefix + originalKey
575	anIstioMetaKey := IstioMetaPrefix + originalKey
576	envs := []string{
577		notIstioMetaKey + "=bar",
578		anIstioMetaKey + "=baz",
579	}
580	nm, _, err := getNodeMetaData(envs, nil, nil, 0, &meshconfig.ProxyConfig{})
581	if err != nil {
582		t.Fatal(err)
583	}
584	if _, ok := nm.Raw[notIstioMetaKey]; ok {
585		t.Fatalf("%s should not be encoded in node metadata", notIstioMetaKey)
586	}
587
588	if _, ok := nm.Raw[anIstioMetaKey]; ok {
589		t.Fatalf("%s should not be encoded in node metadata. The prefix '%s' should be stripped", anIstioMetaKey, IstioMetaPrefix)
590	}
591	if val, ok := nm.Raw[originalKey]; !ok {
592		t.Fatalf("%s has the prefix %s and it should be encoded in the node metadata", originalKey, IstioMetaPrefix)
593	} else if val != "baz" {
594		t.Fatalf("unexpected value node metadata %s. got %s, want: %s", originalKey, val, "baz")
595	}
596}
597
598func TestNodeMetadata(t *testing.T) {
599	envs := []string{
600		"ISTIO_META_ISTIO_VERSION=1.0.0",
601		`ISTIO_METAJSON_LABELS={"foo":"bar"}`,
602	}
603	nm, _, err := getNodeMetaData(envs, nil, nil, 0, &meshconfig.ProxyConfig{})
604	if err != nil {
605		t.Fatal(err)
606	}
607	if nm.IstioVersion != "1.0.0" {
608		t.Fatalf("Expected IstioVersion 1.0.0, got %v", nm.IstioVersion)
609	}
610	if !reflect.DeepEqual(nm.Labels, map[string]string{"foo": "bar"}) {
611		t.Fatalf("Expected Labels foo: bar, got %v", nm.Labels)
612	}
613}
614
615func mergeMap(to map[string]string, from map[string]string) {
616	for k, v := range from {
617		to[k] = v
618	}
619}
620
621type fakePlatform struct {
622	platform.Environment
623
624	meta map[string]string
625}
626
627func (f *fakePlatform) Metadata() map[string]string {
628	return f.meta
629}
630
631func (f *fakePlatform) Locality() *core.Locality {
632	return &core.Locality{}
633}
634