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