1package taskenv
2
3import (
4	"fmt"
5	"os"
6	"reflect"
7	"sort"
8	"strings"
9	"testing"
10
11	"github.com/hashicorp/hcl2/gohcl"
12	"github.com/hashicorp/hcl2/hcl"
13	"github.com/hashicorp/hcl2/hcl/hclsyntax"
14	"github.com/hashicorp/nomad/nomad/mock"
15	"github.com/hashicorp/nomad/nomad/structs"
16	"github.com/hashicorp/nomad/plugins/drivers"
17	"github.com/stretchr/testify/assert"
18	"github.com/stretchr/testify/require"
19)
20
21const (
22	// Node values that tests can rely on
23	metaKey   = "instance"
24	metaVal   = "t2-micro"
25	attrKey   = "arch"
26	attrVal   = "amd64"
27	nodeName  = "test node"
28	nodeClass = "test class"
29
30	// Environment variable values that tests can rely on
31	envOneKey = "NOMAD_IP"
32	envOneVal = "127.0.0.1"
33	envTwoKey = "NOMAD_PORT_WEB"
34	envTwoVal = ":80"
35)
36
37var (
38	// portMap for use in tests as its set after Builder creation
39	portMap = map[string]int{
40		"https": 443,
41	}
42)
43
44func testEnvBuilder() *Builder {
45	n := mock.Node()
46	n.Attributes = map[string]string{
47		attrKey: attrVal,
48	}
49	n.Meta = map[string]string{
50		metaKey: metaVal,
51	}
52	n.Name = nodeName
53	n.NodeClass = nodeClass
54
55	task := mock.Job().TaskGroups[0].Tasks[0]
56	task.Env = map[string]string{
57		envOneKey: envOneVal,
58		envTwoKey: envTwoVal,
59	}
60	return NewBuilder(n, mock.Alloc(), task, "global")
61}
62
63func TestEnvironment_ParseAndReplace_Env(t *testing.T) {
64	env := testEnvBuilder()
65
66	input := []string{fmt.Sprintf(`"${%v}"!`, envOneKey), fmt.Sprintf("${%s}${%s}", envOneKey, envTwoKey)}
67	act := env.Build().ParseAndReplace(input)
68	exp := []string{fmt.Sprintf(`"%s"!`, envOneVal), fmt.Sprintf("%s%s", envOneVal, envTwoVal)}
69
70	if !reflect.DeepEqual(act, exp) {
71		t.Fatalf("ParseAndReplace(%v) returned %#v; want %#v", input, act, exp)
72	}
73}
74
75func TestEnvironment_ParseAndReplace_Meta(t *testing.T) {
76	input := []string{fmt.Sprintf("${%v%v}", nodeMetaPrefix, metaKey)}
77	exp := []string{metaVal}
78	env := testEnvBuilder()
79	act := env.Build().ParseAndReplace(input)
80
81	if !reflect.DeepEqual(act, exp) {
82		t.Fatalf("ParseAndReplace(%v) returned %#v; want %#v", input, act, exp)
83	}
84}
85
86func TestEnvironment_ParseAndReplace_Attr(t *testing.T) {
87	input := []string{fmt.Sprintf("${%v%v}", nodeAttributePrefix, attrKey)}
88	exp := []string{attrVal}
89	env := testEnvBuilder()
90	act := env.Build().ParseAndReplace(input)
91
92	if !reflect.DeepEqual(act, exp) {
93		t.Fatalf("ParseAndReplace(%v) returned %#v; want %#v", input, act, exp)
94	}
95}
96
97func TestEnvironment_ParseAndReplace_Node(t *testing.T) {
98	input := []string{fmt.Sprintf("${%v}", nodeNameKey), fmt.Sprintf("${%v}", nodeClassKey)}
99	exp := []string{nodeName, nodeClass}
100	env := testEnvBuilder()
101	act := env.Build().ParseAndReplace(input)
102
103	if !reflect.DeepEqual(act, exp) {
104		t.Fatalf("ParseAndReplace(%v) returned %#v; want %#v", input, act, exp)
105	}
106}
107
108func TestEnvironment_ParseAndReplace_Mixed(t *testing.T) {
109	input := []string{
110		fmt.Sprintf("${%v}${%v%v}", nodeNameKey, nodeAttributePrefix, attrKey),
111		fmt.Sprintf("${%v}${%v%v}", nodeClassKey, nodeMetaPrefix, metaKey),
112		fmt.Sprintf("${%v}${%v}", envTwoKey, nodeClassKey),
113	}
114	exp := []string{
115		fmt.Sprintf("%v%v", nodeName, attrVal),
116		fmt.Sprintf("%v%v", nodeClass, metaVal),
117		fmt.Sprintf("%v%v", envTwoVal, nodeClass),
118	}
119	env := testEnvBuilder()
120	act := env.Build().ParseAndReplace(input)
121
122	if !reflect.DeepEqual(act, exp) {
123		t.Fatalf("ParseAndReplace(%v) returned %#v; want %#v", input, act, exp)
124	}
125}
126
127func TestEnvironment_ReplaceEnv_Mixed(t *testing.T) {
128	input := fmt.Sprintf("${%v}${%v%v}", nodeNameKey, nodeAttributePrefix, attrKey)
129	exp := fmt.Sprintf("%v%v", nodeName, attrVal)
130	env := testEnvBuilder()
131	act := env.Build().ReplaceEnv(input)
132
133	if act != exp {
134		t.Fatalf("ParseAndReplace(%v) returned %#v; want %#v", input, act, exp)
135	}
136}
137
138func TestEnvironment_AsList(t *testing.T) {
139	n := mock.Node()
140	n.Meta = map[string]string{
141		"metaKey": "metaVal",
142	}
143	a := mock.Alloc()
144	a.AllocatedResources.Tasks["web"].Networks[0] = &structs.NetworkResource{
145		Device:        "eth0",
146		IP:            "127.0.0.1",
147		ReservedPorts: []structs.Port{{Label: "https", Value: 8080}},
148		MBits:         50,
149		DynamicPorts:  []structs.Port{{Label: "http", Value: 80}},
150	}
151	a.AllocatedResources.Tasks["ssh"] = &structs.AllocatedTaskResources{
152		Networks: []*structs.NetworkResource{
153			{
154				Device: "eth0",
155				IP:     "192.168.0.100",
156				MBits:  50,
157				ReservedPorts: []structs.Port{
158					{Label: "ssh", Value: 22},
159					{Label: "other", Value: 1234},
160				},
161			},
162		},
163	}
164	a.Namespace = "not-default"
165	task := a.Job.TaskGroups[0].Tasks[0]
166	task.Env = map[string]string{
167		"taskEnvKey": "taskEnvVal",
168	}
169	env := NewBuilder(n, a, task, "global").SetDriverNetwork(
170		&drivers.DriverNetwork{PortMap: map[string]int{"https": 443}},
171	)
172
173	act := env.Build().List()
174	exp := []string{
175		"taskEnvKey=taskEnvVal",
176		"NOMAD_ADDR_http=127.0.0.1:80",
177		"NOMAD_PORT_http=80",
178		"NOMAD_IP_http=127.0.0.1",
179		"NOMAD_ADDR_https=127.0.0.1:8080",
180		"NOMAD_PORT_https=443",
181		"NOMAD_IP_https=127.0.0.1",
182		"NOMAD_HOST_PORT_http=80",
183		"NOMAD_HOST_PORT_https=8080",
184		"NOMAD_TASK_NAME=web",
185		"NOMAD_GROUP_NAME=web",
186		"NOMAD_ADDR_ssh_other=192.168.0.100:1234",
187		"NOMAD_ADDR_ssh_ssh=192.168.0.100:22",
188		"NOMAD_IP_ssh_other=192.168.0.100",
189		"NOMAD_IP_ssh_ssh=192.168.0.100",
190		"NOMAD_PORT_ssh_other=1234",
191		"NOMAD_PORT_ssh_ssh=22",
192		"NOMAD_CPU_LIMIT=500",
193		"NOMAD_DC=dc1",
194		"NOMAD_NAMESPACE=not-default",
195		"NOMAD_REGION=global",
196		"NOMAD_MEMORY_LIMIT=256",
197		"NOMAD_META_ELB_CHECK_INTERVAL=30s",
198		"NOMAD_META_ELB_CHECK_MIN=3",
199		"NOMAD_META_ELB_CHECK_TYPE=http",
200		"NOMAD_META_FOO=bar",
201		"NOMAD_META_OWNER=armon",
202		"NOMAD_META_elb_check_interval=30s",
203		"NOMAD_META_elb_check_min=3",
204		"NOMAD_META_elb_check_type=http",
205		"NOMAD_META_foo=bar",
206		"NOMAD_META_owner=armon",
207		"NOMAD_JOB_NAME=my-job",
208		fmt.Sprintf("NOMAD_ALLOC_ID=%s", a.ID),
209		"NOMAD_ALLOC_INDEX=0",
210	}
211	sort.Strings(act)
212	sort.Strings(exp)
213	require.Equal(t, exp, act)
214}
215
216// COMPAT(0.11): Remove in 0.11
217func TestEnvironment_AsList_Old(t *testing.T) {
218	n := mock.Node()
219	n.Meta = map[string]string{
220		"metaKey": "metaVal",
221	}
222	a := mock.Alloc()
223	a.AllocatedResources = nil
224	a.Resources = &structs.Resources{
225		CPU:      500,
226		MemoryMB: 256,
227		DiskMB:   150,
228		Networks: []*structs.NetworkResource{
229			{
230				Device: "eth0",
231				IP:     "192.168.0.100",
232				ReservedPorts: []structs.Port{
233					{Label: "ssh", Value: 22},
234					{Label: "other", Value: 1234},
235				},
236				MBits:        50,
237				DynamicPorts: []structs.Port{{Label: "http", Value: 2000}},
238			},
239		},
240	}
241	a.TaskResources = map[string]*structs.Resources{
242		"web": {
243			CPU:      500,
244			MemoryMB: 256,
245			Networks: []*structs.NetworkResource{
246				{
247					Device:        "eth0",
248					IP:            "127.0.0.1",
249					ReservedPorts: []structs.Port{{Label: "https", Value: 8080}},
250					MBits:         50,
251					DynamicPorts:  []structs.Port{{Label: "http", Value: 80}},
252				},
253			},
254		},
255	}
256	a.TaskResources["ssh"] = &structs.Resources{
257		Networks: []*structs.NetworkResource{
258			{
259				Device: "eth0",
260				IP:     "192.168.0.100",
261				MBits:  50,
262				ReservedPorts: []structs.Port{
263					{Label: "ssh", Value: 22},
264					{Label: "other", Value: 1234},
265				},
266			},
267		},
268	}
269	task := a.Job.TaskGroups[0].Tasks[0]
270	task.Env = map[string]string{
271		"taskEnvKey": "taskEnvVal",
272	}
273	task.Resources.Networks = []*structs.NetworkResource{
274		// Nomad 0.8 didn't fully populate the fields in task Resource Networks
275		{
276			IP:            "",
277			ReservedPorts: []structs.Port{{Label: "https"}},
278			DynamicPorts:  []structs.Port{{Label: "http"}},
279		},
280	}
281	env := NewBuilder(n, a, task, "global").SetDriverNetwork(
282		&drivers.DriverNetwork{PortMap: map[string]int{"https": 443}},
283	)
284
285	act := env.Build().List()
286	exp := []string{
287		"taskEnvKey=taskEnvVal",
288		"NOMAD_ADDR_http=127.0.0.1:80",
289		"NOMAD_PORT_http=80",
290		"NOMAD_IP_http=127.0.0.1",
291		"NOMAD_ADDR_https=127.0.0.1:8080",
292		"NOMAD_PORT_https=443",
293		"NOMAD_IP_https=127.0.0.1",
294		"NOMAD_HOST_PORT_http=80",
295		"NOMAD_HOST_PORT_https=8080",
296		"NOMAD_TASK_NAME=web",
297		"NOMAD_GROUP_NAME=web",
298		"NOMAD_ADDR_ssh_other=192.168.0.100:1234",
299		"NOMAD_ADDR_ssh_ssh=192.168.0.100:22",
300		"NOMAD_IP_ssh_other=192.168.0.100",
301		"NOMAD_IP_ssh_ssh=192.168.0.100",
302		"NOMAD_PORT_ssh_other=1234",
303		"NOMAD_PORT_ssh_ssh=22",
304		"NOMAD_CPU_LIMIT=500",
305		"NOMAD_DC=dc1",
306		"NOMAD_NAMESPACE=default",
307		"NOMAD_REGION=global",
308		"NOMAD_MEMORY_LIMIT=256",
309		"NOMAD_META_ELB_CHECK_INTERVAL=30s",
310		"NOMAD_META_ELB_CHECK_MIN=3",
311		"NOMAD_META_ELB_CHECK_TYPE=http",
312		"NOMAD_META_FOO=bar",
313		"NOMAD_META_OWNER=armon",
314		"NOMAD_META_elb_check_interval=30s",
315		"NOMAD_META_elb_check_min=3",
316		"NOMAD_META_elb_check_type=http",
317		"NOMAD_META_foo=bar",
318		"NOMAD_META_owner=armon",
319		"NOMAD_JOB_NAME=my-job",
320		fmt.Sprintf("NOMAD_ALLOC_ID=%s", a.ID),
321		"NOMAD_ALLOC_INDEX=0",
322	}
323	sort.Strings(act)
324	sort.Strings(exp)
325	require.Equal(t, exp, act)
326}
327
328func TestEnvironment_AllValues(t *testing.T) {
329	t.Parallel()
330
331	n := mock.Node()
332	n.Meta = map[string]string{
333		"metaKey":           "metaVal",
334		"nested.meta.key":   "a",
335		"invalid...metakey": "b",
336	}
337	a := mock.ConnectAlloc()
338	a.AllocatedResources.Tasks["web"].Networks[0] = &structs.NetworkResource{
339		Device:        "eth0",
340		IP:            "127.0.0.1",
341		ReservedPorts: []structs.Port{{Label: "https", Value: 8080}},
342		MBits:         50,
343		DynamicPorts:  []structs.Port{{Label: "http", Value: 80}},
344	}
345	a.AllocatedResources.Tasks["ssh"] = &structs.AllocatedTaskResources{
346		Networks: []*structs.NetworkResource{
347			{
348				Device: "eth0",
349				IP:     "192.168.0.100",
350				MBits:  50,
351				ReservedPorts: []structs.Port{
352					{Label: "ssh", Value: 22},
353					{Label: "other", Value: 1234},
354				},
355			},
356		},
357	}
358
359	sharedNet := a.AllocatedResources.Shared.Networks[0]
360
361	// Add group network port with only a host port.
362	sharedNet.DynamicPorts = append(sharedNet.DynamicPorts, structs.Port{
363		Label: "hostonly",
364		Value: 9998,
365	})
366
367	// Add group network reserved port with a To value.
368	sharedNet.ReservedPorts = append(sharedNet.ReservedPorts, structs.Port{
369		Label: "static",
370		Value: 9997,
371		To:    97,
372	})
373
374	task := a.Job.TaskGroups[0].Tasks[0]
375	task.Env = map[string]string{
376		"taskEnvKey":        "taskEnvVal",
377		"nested.task.key":   "x",
378		"invalid...taskkey": "y",
379		".a":                "a",
380		"b.":                "b",
381		".":                 "c",
382	}
383	env := NewBuilder(n, a, task, "global").SetDriverNetwork(
384		&drivers.DriverNetwork{PortMap: map[string]int{"https": 443}},
385	)
386
387	values, errs, err := env.Build().AllValues()
388	require.NoError(t, err)
389
390	// Assert the keys we couldn't nest were reported
391	require.Len(t, errs, 5)
392	require.Contains(t, errs, "invalid...taskkey")
393	require.Contains(t, errs, "meta.invalid...metakey")
394	require.Contains(t, errs, ".a")
395	require.Contains(t, errs, "b.")
396	require.Contains(t, errs, ".")
397
398	exp := map[string]string{
399		// Node
400		"node.unique.id":          n.ID,
401		"node.region":             "global",
402		"node.datacenter":         n.Datacenter,
403		"node.unique.name":        n.Name,
404		"node.class":              n.NodeClass,
405		"meta.metaKey":            "metaVal",
406		"attr.arch":               "x86",
407		"attr.driver.exec":        "1",
408		"attr.driver.mock_driver": "1",
409		"attr.kernel.name":        "linux",
410		"attr.nomad.version":      "0.5.0",
411
412		// 0.9 style meta and attr
413		"node.meta.metaKey":            "metaVal",
414		"node.attr.arch":               "x86",
415		"node.attr.driver.exec":        "1",
416		"node.attr.driver.mock_driver": "1",
417		"node.attr.kernel.name":        "linux",
418		"node.attr.nomad.version":      "0.5.0",
419
420		// Env
421		"taskEnvKey":                                "taskEnvVal",
422		"NOMAD_ADDR_http":                           "127.0.0.1:80",
423		"NOMAD_PORT_http":                           "80",
424		"NOMAD_IP_http":                             "127.0.0.1",
425		"NOMAD_ADDR_https":                          "127.0.0.1:8080",
426		"NOMAD_PORT_https":                          "443",
427		"NOMAD_IP_https":                            "127.0.0.1",
428		"NOMAD_HOST_PORT_http":                      "80",
429		"NOMAD_HOST_PORT_https":                     "8080",
430		"NOMAD_TASK_NAME":                           "web",
431		"NOMAD_GROUP_NAME":                          "web",
432		"NOMAD_ADDR_ssh_other":                      "192.168.0.100:1234",
433		"NOMAD_ADDR_ssh_ssh":                        "192.168.0.100:22",
434		"NOMAD_IP_ssh_other":                        "192.168.0.100",
435		"NOMAD_IP_ssh_ssh":                          "192.168.0.100",
436		"NOMAD_PORT_ssh_other":                      "1234",
437		"NOMAD_PORT_ssh_ssh":                        "22",
438		"NOMAD_CPU_LIMIT":                           "500",
439		"NOMAD_DC":                                  "dc1",
440		"NOMAD_NAMESPACE":                           "default",
441		"NOMAD_REGION":                              "global",
442		"NOMAD_MEMORY_LIMIT":                        "256",
443		"NOMAD_META_ELB_CHECK_INTERVAL":             "30s",
444		"NOMAD_META_ELB_CHECK_MIN":                  "3",
445		"NOMAD_META_ELB_CHECK_TYPE":                 "http",
446		"NOMAD_META_FOO":                            "bar",
447		"NOMAD_META_OWNER":                          "armon",
448		"NOMAD_META_elb_check_interval":             "30s",
449		"NOMAD_META_elb_check_min":                  "3",
450		"NOMAD_META_elb_check_type":                 "http",
451		"NOMAD_META_foo":                            "bar",
452		"NOMAD_META_owner":                          "armon",
453		"NOMAD_JOB_NAME":                            "my-job",
454		"NOMAD_ALLOC_ID":                            a.ID,
455		"NOMAD_ALLOC_INDEX":                         "0",
456		"NOMAD_PORT_connect_proxy_testconnect":      "9999",
457		"NOMAD_HOST_PORT_connect_proxy_testconnect": "9999",
458		"NOMAD_PORT_hostonly":                       "9998",
459		"NOMAD_HOST_PORT_hostonly":                  "9998",
460		"NOMAD_PORT_static":                         "97",
461		"NOMAD_HOST_PORT_static":                    "9997",
462
463		// 0.9 style env map
464		`env["taskEnvKey"]`:        "taskEnvVal",
465		`env["NOMAD_ADDR_http"]`:   "127.0.0.1:80",
466		`env["nested.task.key"]`:   "x",
467		`env["invalid...taskkey"]`: "y",
468		`env[".a"]`:                "a",
469		`env["b."]`:                "b",
470		`env["."]`:                 "c",
471	}
472
473	evalCtx := &hcl.EvalContext{
474		Variables: values,
475	}
476
477	for k, expectedVal := range exp {
478		t.Run(k, func(t *testing.T) {
479			// Parse HCL containing the test key
480			hclStr := fmt.Sprintf(`"${%s}"`, k)
481			expr, diag := hclsyntax.ParseExpression([]byte(hclStr), "test.hcl", hcl.Pos{})
482			require.Empty(t, diag)
483
484			// Decode with the TaskEnv values
485			out := ""
486			diag = gohcl.DecodeExpression(expr, evalCtx, &out)
487			require.Empty(t, diag)
488			require.Equal(t, out, expectedVal)
489		})
490	}
491}
492
493func TestEnvironment_VaultToken(t *testing.T) {
494	n := mock.Node()
495	a := mock.Alloc()
496	env := NewBuilder(n, a, a.Job.TaskGroups[0].Tasks[0], "global")
497	env.SetVaultToken("123", "vault-namespace", false)
498
499	{
500		act := env.Build().All()
501		if act[VaultToken] != "" {
502			t.Fatalf("Unexpected environment variables: %s=%q", VaultToken, act[VaultToken])
503		}
504		if act[VaultNamespace] != "" {
505			t.Fatalf("Unexpected environment variables: %s=%q", VaultNamespace, act[VaultNamespace])
506		}
507	}
508
509	{
510		act := env.SetVaultToken("123", "", true).Build().List()
511		exp := "VAULT_TOKEN=123"
512		found := false
513		foundNs := false
514		for _, entry := range act {
515			if entry == exp {
516				found = true
517			}
518			if strings.HasPrefix(entry, "VAULT_NAMESPACE=") {
519				foundNs = true
520			}
521		}
522		if !found {
523			t.Fatalf("did not find %q in:\n%s", exp, strings.Join(act, "\n"))
524		}
525		if foundNs {
526			t.Fatalf("found unwanted VAULT_NAMESPACE in:\n%s", strings.Join(act, "\n"))
527		}
528	}
529
530	{
531		act := env.SetVaultToken("123", "vault-namespace", true).Build().List()
532		exp := "VAULT_TOKEN=123"
533		expNs := "VAULT_NAMESPACE=vault-namespace"
534		found := false
535		foundNs := false
536		for _, entry := range act {
537			if entry == exp {
538				found = true
539			}
540			if entry == expNs {
541				foundNs = true
542			}
543		}
544		if !found {
545			t.Fatalf("did not find %q in:\n%s", exp, strings.Join(act, "\n"))
546		}
547		if !foundNs {
548			t.Fatalf("did not find %q in:\n%s", expNs, strings.Join(act, "\n"))
549		}
550	}
551}
552
553func TestEnvironment_Envvars(t *testing.T) {
554	envMap := map[string]string{"foo": "baz", "bar": "bang"}
555	n := mock.Node()
556	a := mock.Alloc()
557	task := a.Job.TaskGroups[0].Tasks[0]
558	task.Env = envMap
559	net := &drivers.DriverNetwork{PortMap: portMap}
560	act := NewBuilder(n, a, task, "global").SetDriverNetwork(net).Build().All()
561	for k, v := range envMap {
562		actV, ok := act[k]
563		if !ok {
564			t.Fatalf("missing %q in %#v", k, act)
565		}
566		if v != actV {
567			t.Fatalf("expected %s=%q but found %q", k, v, actV)
568		}
569	}
570}
571
572// TestEnvironment_HookVars asserts hook env vars are LWW and deletes of later
573// writes allow earlier hook's values to be visible.
574func TestEnvironment_HookVars(t *testing.T) {
575	n := mock.Node()
576	a := mock.Alloc()
577	builder := NewBuilder(n, a, a.Job.TaskGroups[0].Tasks[0], "global")
578
579	// Add vars from two hooks and assert the second one wins on
580	// conflicting keys.
581	builder.SetHookEnv("hookA", map[string]string{
582		"foo": "bar",
583		"baz": "quux",
584	})
585	builder.SetHookEnv("hookB", map[string]string{
586		"foo":   "123",
587		"hookB": "wins",
588	})
589
590	{
591		out := builder.Build().All()
592		assert.Equal(t, "123", out["foo"])
593		assert.Equal(t, "quux", out["baz"])
594		assert.Equal(t, "wins", out["hookB"])
595	}
596
597	// Asserting overwriting hook vars allows the first hooks original
598	// value to be used.
599	builder.SetHookEnv("hookB", nil)
600	{
601		out := builder.Build().All()
602		assert.Equal(t, "bar", out["foo"])
603		assert.Equal(t, "quux", out["baz"])
604		assert.NotContains(t, out, "hookB")
605	}
606}
607
608// TestEnvironment_DeviceHookVars asserts device hook env vars are accessible
609// separately.
610func TestEnvironment_DeviceHookVars(t *testing.T) {
611	require := require.New(t)
612	n := mock.Node()
613	a := mock.Alloc()
614	builder := NewBuilder(n, a, a.Job.TaskGroups[0].Tasks[0], "global")
615
616	// Add vars from two hooks and assert the second one wins on
617	// conflicting keys.
618	builder.SetHookEnv("hookA", map[string]string{
619		"foo": "bar",
620		"baz": "quux",
621	})
622	builder.SetDeviceHookEnv("devices", map[string]string{
623		"hook": "wins",
624	})
625
626	b := builder.Build()
627	deviceEnv := b.DeviceEnv()
628	require.Len(deviceEnv, 1)
629	require.Contains(deviceEnv, "hook")
630
631	all := b.Map()
632	require.Contains(all, "foo")
633}
634
635func TestEnvironment_Interpolate(t *testing.T) {
636	n := mock.Node()
637	n.Attributes["arch"] = "x86"
638	n.NodeClass = "test class"
639	a := mock.Alloc()
640	task := a.Job.TaskGroups[0].Tasks[0]
641	task.Env = map[string]string{"test": "${node.class}", "test2": "${attr.arch}"}
642	env := NewBuilder(n, a, task, "global").Build()
643
644	exp := []string{fmt.Sprintf("test=%s", n.NodeClass), fmt.Sprintf("test2=%s", n.Attributes["arch"])}
645	found1, found2 := false, false
646	for _, entry := range env.List() {
647		switch entry {
648		case exp[0]:
649			found1 = true
650		case exp[1]:
651			found2 = true
652		}
653	}
654	if !found1 || !found2 {
655		t.Fatalf("expected to find %q and %q but got:\n%s",
656			exp[0], exp[1], strings.Join(env.List(), "\n"))
657	}
658}
659
660func TestEnvironment_AppendHostEnvvars(t *testing.T) {
661	host := os.Environ()
662	if len(host) < 2 {
663		t.Skip("No host environment variables. Can't test")
664	}
665	skip := strings.Split(host[0], "=")[0]
666	env := testEnvBuilder().
667		SetHostEnvvars([]string{skip}).
668		Build()
669
670	act := env.Map()
671	if len(act) < 1 {
672		t.Fatalf("Host environment variables not properly set")
673	}
674	if _, ok := act[skip]; ok {
675		t.Fatalf("Didn't filter environment variable %q", skip)
676	}
677}
678
679// TestEnvironment_DashesInTaskName asserts dashes in port labels are properly
680// converted to underscores in environment variables.
681// See: https://github.com/hashicorp/nomad/issues/2405
682func TestEnvironment_DashesInTaskName(t *testing.T) {
683	a := mock.Alloc()
684	task := a.Job.TaskGroups[0].Tasks[0]
685	task.Env = map[string]string{
686		"test-one-two":       "three-four",
687		"NOMAD_test_one_two": "three-five",
688	}
689	envMap := NewBuilder(mock.Node(), a, task, "global").Build().Map()
690
691	if envMap["test-one-two"] != "three-four" {
692		t.Fatalf("Expected test-one-two=three-four in TaskEnv; found:\n%#v", envMap)
693	}
694	if envMap["NOMAD_test_one_two"] != "three-five" {
695		t.Fatalf("Expected NOMAD_test_one_two=three-five in TaskEnv; found:\n%#v", envMap)
696	}
697}
698
699// TestEnvironment_UpdateTask asserts env vars and task meta are updated when a
700// task is updated.
701func TestEnvironment_UpdateTask(t *testing.T) {
702	a := mock.Alloc()
703	a.Job.TaskGroups[0].Meta = map[string]string{"tgmeta": "tgmetaval"}
704	task := a.Job.TaskGroups[0].Tasks[0]
705	task.Name = "orig"
706	task.Env = map[string]string{"env": "envval"}
707	task.Meta = map[string]string{"taskmeta": "taskmetaval"}
708	builder := NewBuilder(mock.Node(), a, task, "global")
709
710	origMap := builder.Build().Map()
711	if origMap["NOMAD_TASK_NAME"] != "orig" {
712		t.Errorf("Expected NOMAD_TASK_NAME=orig but found %q", origMap["NOMAD_TASK_NAME"])
713	}
714	if origMap["NOMAD_META_taskmeta"] != "taskmetaval" {
715		t.Errorf("Expected NOMAD_META_taskmeta=taskmetaval but found %q", origMap["NOMAD_META_taskmeta"])
716	}
717	if origMap["env"] != "envval" {
718		t.Errorf("Expected env=envva but found %q", origMap["env"])
719	}
720	if origMap["NOMAD_META_tgmeta"] != "tgmetaval" {
721		t.Errorf("Expected NOMAD_META_tgmeta=tgmetaval but found %q", origMap["NOMAD_META_tgmeta"])
722	}
723
724	a.Job.TaskGroups[0].Meta = map[string]string{"tgmeta2": "tgmetaval2"}
725	task.Name = "new"
726	task.Env = map[string]string{"env2": "envval2"}
727	task.Meta = map[string]string{"taskmeta2": "taskmetaval2"}
728
729	newMap := builder.UpdateTask(a, task).Build().Map()
730	if newMap["NOMAD_TASK_NAME"] != "new" {
731		t.Errorf("Expected NOMAD_TASK_NAME=new but found %q", newMap["NOMAD_TASK_NAME"])
732	}
733	if newMap["NOMAD_META_taskmeta2"] != "taskmetaval2" {
734		t.Errorf("Expected NOMAD_META_taskmeta=taskmetaval but found %q", newMap["NOMAD_META_taskmeta2"])
735	}
736	if newMap["env2"] != "envval2" {
737		t.Errorf("Expected env=envva but found %q", newMap["env2"])
738	}
739	if newMap["NOMAD_META_tgmeta2"] != "tgmetaval2" {
740		t.Errorf("Expected NOMAD_META_tgmeta=tgmetaval but found %q", newMap["NOMAD_META_tgmeta2"])
741	}
742	if v, ok := newMap["NOMAD_META_taskmeta"]; ok {
743		t.Errorf("Expected NOMAD_META_taskmeta to be unset but found: %q", v)
744	}
745}
746
747// TestEnvironment_InterpolateEmptyOptionalMeta asserts that in a parameterized
748// job, if an optional meta field is not set, it will get interpolated as an
749// empty string.
750func TestEnvironment_InterpolateEmptyOptionalMeta(t *testing.T) {
751	require := require.New(t)
752	a := mock.Alloc()
753	a.Job.ParameterizedJob = &structs.ParameterizedJobConfig{
754		MetaOptional: []string{"metaopt1", "metaopt2"},
755	}
756	a.Job.Dispatched = true
757	task := a.Job.TaskGroups[0].Tasks[0]
758	task.Meta = map[string]string{"metaopt1": "metaopt1val"}
759	env := NewBuilder(mock.Node(), a, task, "global").Build()
760	require.Equal("metaopt1val", env.ReplaceEnv("${NOMAD_META_metaopt1}"))
761	require.Empty(env.ReplaceEnv("${NOMAD_META_metaopt2}"))
762}
763
764// TestEnvironment_Upsteams asserts that group.service.upstreams entries are
765// added to the environment.
766func TestEnvironment_Upstreams(t *testing.T) {
767	t.Parallel()
768
769	// Add some upstreams to the mock alloc
770	a := mock.Alloc()
771	tg := a.Job.LookupTaskGroup(a.TaskGroup)
772	tg.Services = []*structs.Service{
773		// Services without Connect should be ignored
774		{
775			Name: "ignoreme",
776		},
777		// All upstreams from a service should be added
778		{
779			Name: "remote_service",
780			Connect: &structs.ConsulConnect{
781				SidecarService: &structs.ConsulSidecarService{
782					Proxy: &structs.ConsulProxy{
783						Upstreams: []structs.ConsulUpstream{
784							{
785								DestinationName: "foo-bar",
786								LocalBindPort:   1234,
787							},
788							{
789								DestinationName: "bar",
790								LocalBindPort:   5678,
791							},
792						},
793					},
794				},
795			},
796		},
797	}
798
799	// Ensure the upstreams can be interpolated
800	tg.Tasks[0].Env = map[string]string{
801		"foo": "${NOMAD_UPSTREAM_ADDR_foo_bar}",
802		"bar": "${NOMAD_UPSTREAM_PORT_foo-bar}",
803	}
804
805	env := NewBuilder(mock.Node(), a, tg.Tasks[0], "global").Build().Map()
806	require.Equal(t, "127.0.0.1:1234", env["NOMAD_UPSTREAM_ADDR_foo_bar"])
807	require.Equal(t, "127.0.0.1", env["NOMAD_UPSTREAM_IP_foo_bar"])
808	require.Equal(t, "1234", env["NOMAD_UPSTREAM_PORT_foo_bar"])
809	require.Equal(t, "127.0.0.1:5678", env["NOMAD_UPSTREAM_ADDR_bar"])
810	require.Equal(t, "127.0.0.1", env["NOMAD_UPSTREAM_IP_bar"])
811	require.Equal(t, "5678", env["NOMAD_UPSTREAM_PORT_bar"])
812	require.Equal(t, "127.0.0.1:1234", env["foo"])
813	require.Equal(t, "1234", env["bar"])
814}
815
816func TestEnvironment_SetPortMapEnvs(t *testing.T) {
817	envs := map[string]string{
818		"foo":            "bar",
819		"NOMAD_PORT_ssh": "2342",
820	}
821	ports := map[string]int{
822		"ssh":  22,
823		"http": 80,
824	}
825
826	envs = SetPortMapEnvs(envs, ports)
827
828	expected := map[string]string{
829		"foo":             "bar",
830		"NOMAD_PORT_ssh":  "22",
831		"NOMAD_PORT_http": "80",
832	}
833	require.Equal(t, expected, envs)
834}
835
836func TestEnvironment_TasklessBuilder(t *testing.T) {
837	node := mock.Node()
838	alloc := mock.Alloc()
839	alloc.Job.Meta["jobt"] = "foo"
840	alloc.Job.TaskGroups[0].Meta["groupt"] = "bar"
841	require := require.New(t)
842	var taskEnv *TaskEnv
843	require.NotPanics(func() {
844		taskEnv = NewBuilder(node, alloc, nil, "global").SetAllocDir("/tmp/alloc").Build()
845	})
846
847	require.Equal("foo", taskEnv.ReplaceEnv("${NOMAD_META_jobt}"))
848	require.Equal("bar", taskEnv.ReplaceEnv("${NOMAD_META_groupt}"))
849}
850