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