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