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  }