github.com/bigcommerce/nomad@v0.9.3-bc/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  	"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  
    21  const (
    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  
    37  var (
    38  	// portMap for use in tests as its set after Builder creation
    39  	portMap = map[string]int{
    40  		"https": 443,
    41  	}
    42  )
    43  
    44  func 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  
    63  func 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  
    75  func 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  
    86  func 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  
    97  func 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  
   108  func 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  
   127  func 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  
   138  func 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  	task := a.Job.TaskGroups[0].Tasks[0]
   165  	task.Env = map[string]string{
   166  		"taskEnvKey": "taskEnvVal",
   167  	}
   168  	env := NewBuilder(n, a, task, "global").SetDriverNetwork(
   169  		&drivers.DriverNetwork{PortMap: map[string]int{"https": 443}},
   170  	)
   171  
   172  	act := env.Build().List()
   173  	exp := []string{
   174  		"taskEnvKey=taskEnvVal",
   175  		"NOMAD_ADDR_http=127.0.0.1:80",
   176  		"NOMAD_PORT_http=80",
   177  		"NOMAD_IP_http=127.0.0.1",
   178  		"NOMAD_ADDR_https=127.0.0.1:8080",
   179  		"NOMAD_PORT_https=443",
   180  		"NOMAD_IP_https=127.0.0.1",
   181  		"NOMAD_HOST_PORT_http=80",
   182  		"NOMAD_HOST_PORT_https=8080",
   183  		"NOMAD_TASK_NAME=web",
   184  		"NOMAD_GROUP_NAME=web",
   185  		"NOMAD_ADDR_ssh_other=192.168.0.100:1234",
   186  		"NOMAD_ADDR_ssh_ssh=192.168.0.100:22",
   187  		"NOMAD_IP_ssh_other=192.168.0.100",
   188  		"NOMAD_IP_ssh_ssh=192.168.0.100",
   189  		"NOMAD_PORT_ssh_other=1234",
   190  		"NOMAD_PORT_ssh_ssh=22",
   191  		"NOMAD_CPU_LIMIT=500",
   192  		"NOMAD_DC=dc1",
   193  		"NOMAD_REGION=global",
   194  		"NOMAD_MEMORY_LIMIT=256",
   195  		"NOMAD_META_ELB_CHECK_INTERVAL=30s",
   196  		"NOMAD_META_ELB_CHECK_MIN=3",
   197  		"NOMAD_META_ELB_CHECK_TYPE=http",
   198  		"NOMAD_META_FOO=bar",
   199  		"NOMAD_META_OWNER=armon",
   200  		"NOMAD_META_elb_check_interval=30s",
   201  		"NOMAD_META_elb_check_min=3",
   202  		"NOMAD_META_elb_check_type=http",
   203  		"NOMAD_META_foo=bar",
   204  		"NOMAD_META_owner=armon",
   205  		"NOMAD_JOB_NAME=my-job",
   206  		fmt.Sprintf("NOMAD_ALLOC_ID=%s", a.ID),
   207  		"NOMAD_ALLOC_INDEX=0",
   208  	}
   209  	sort.Strings(act)
   210  	sort.Strings(exp)
   211  	require.Equal(t, exp, act)
   212  }
   213  
   214  // COMPAT(0.11): Remove in 0.11
   215  func TestEnvironment_AsList_Old(t *testing.T) {
   216  	n := mock.Node()
   217  	n.Meta = map[string]string{
   218  		"metaKey": "metaVal",
   219  	}
   220  	a := mock.Alloc()
   221  	a.AllocatedResources = nil
   222  	a.Resources = &structs.Resources{
   223  		CPU:      500,
   224  		MemoryMB: 256,
   225  		DiskMB:   150,
   226  		Networks: []*structs.NetworkResource{
   227  			{
   228  				Device: "eth0",
   229  				IP:     "192.168.0.100",
   230  				ReservedPorts: []structs.Port{
   231  					{Label: "ssh", Value: 22},
   232  					{Label: "other", Value: 1234},
   233  				},
   234  				MBits:        50,
   235  				DynamicPorts: []structs.Port{{Label: "http", Value: 2000}},
   236  			},
   237  		},
   238  	}
   239  	a.TaskResources = map[string]*structs.Resources{
   240  		"web": {
   241  			CPU:      500,
   242  			MemoryMB: 256,
   243  			Networks: []*structs.NetworkResource{
   244  				{
   245  					Device:        "eth0",
   246  					IP:            "127.0.0.1",
   247  					ReservedPorts: []structs.Port{{Label: "https", Value: 8080}},
   248  					MBits:         50,
   249  					DynamicPorts:  []structs.Port{{Label: "http", Value: 80}},
   250  				},
   251  			},
   252  		},
   253  	}
   254  	a.TaskResources["ssh"] = &structs.Resources{
   255  		Networks: []*structs.NetworkResource{
   256  			{
   257  				Device: "eth0",
   258  				IP:     "192.168.0.100",
   259  				MBits:  50,
   260  				ReservedPorts: []structs.Port{
   261  					{Label: "ssh", Value: 22},
   262  					{Label: "other", Value: 1234},
   263  				},
   264  			},
   265  		},
   266  	}
   267  	task := a.Job.TaskGroups[0].Tasks[0]
   268  	task.Env = map[string]string{
   269  		"taskEnvKey": "taskEnvVal",
   270  	}
   271  	task.Resources.Networks = []*structs.NetworkResource{
   272  		// Nomad 0.8 didn't fully populate the fields in task Resource Networks
   273  		{
   274  			IP:            "",
   275  			ReservedPorts: []structs.Port{{Label: "https"}},
   276  			DynamicPorts:  []structs.Port{{Label: "http"}},
   277  		},
   278  	}
   279  	env := NewBuilder(n, a, task, "global").SetDriverNetwork(
   280  		&drivers.DriverNetwork{PortMap: map[string]int{"https": 443}},
   281  	)
   282  
   283  	act := env.Build().List()
   284  	exp := []string{
   285  		"taskEnvKey=taskEnvVal",
   286  		"NOMAD_ADDR_http=127.0.0.1:80",
   287  		"NOMAD_PORT_http=80",
   288  		"NOMAD_IP_http=127.0.0.1",
   289  		"NOMAD_ADDR_https=127.0.0.1:8080",
   290  		"NOMAD_PORT_https=443",
   291  		"NOMAD_IP_https=127.0.0.1",
   292  		"NOMAD_HOST_PORT_http=80",
   293  		"NOMAD_HOST_PORT_https=8080",
   294  		"NOMAD_TASK_NAME=web",
   295  		"NOMAD_GROUP_NAME=web",
   296  		"NOMAD_ADDR_ssh_other=192.168.0.100:1234",
   297  		"NOMAD_ADDR_ssh_ssh=192.168.0.100:22",
   298  		"NOMAD_IP_ssh_other=192.168.0.100",
   299  		"NOMAD_IP_ssh_ssh=192.168.0.100",
   300  		"NOMAD_PORT_ssh_other=1234",
   301  		"NOMAD_PORT_ssh_ssh=22",
   302  		"NOMAD_CPU_LIMIT=500",
   303  		"NOMAD_DC=dc1",
   304  		"NOMAD_REGION=global",
   305  		"NOMAD_MEMORY_LIMIT=256",
   306  		"NOMAD_META_ELB_CHECK_INTERVAL=30s",
   307  		"NOMAD_META_ELB_CHECK_MIN=3",
   308  		"NOMAD_META_ELB_CHECK_TYPE=http",
   309  		"NOMAD_META_FOO=bar",
   310  		"NOMAD_META_OWNER=armon",
   311  		"NOMAD_META_elb_check_interval=30s",
   312  		"NOMAD_META_elb_check_min=3",
   313  		"NOMAD_META_elb_check_type=http",
   314  		"NOMAD_META_foo=bar",
   315  		"NOMAD_META_owner=armon",
   316  		"NOMAD_JOB_NAME=my-job",
   317  		fmt.Sprintf("NOMAD_ALLOC_ID=%s", a.ID),
   318  		"NOMAD_ALLOC_INDEX=0",
   319  	}
   320  	sort.Strings(act)
   321  	sort.Strings(exp)
   322  	require.Equal(t, exp, act)
   323  }
   324  
   325  func TestEnvironment_AllValues(t *testing.T) {
   326  	t.Parallel()
   327  
   328  	n := mock.Node()
   329  	n.Meta = map[string]string{
   330  		"metaKey":           "metaVal",
   331  		"nested.meta.key":   "a",
   332  		"invalid...metakey": "b",
   333  	}
   334  	a := mock.Alloc()
   335  	a.AllocatedResources.Tasks["web"].Networks[0] = &structs.NetworkResource{
   336  		Device:        "eth0",
   337  		IP:            "127.0.0.1",
   338  		ReservedPorts: []structs.Port{{Label: "https", Value: 8080}},
   339  		MBits:         50,
   340  		DynamicPorts:  []structs.Port{{Label: "http", Value: 80}},
   341  	}
   342  	a.AllocatedResources.Tasks["ssh"] = &structs.AllocatedTaskResources{
   343  		Networks: []*structs.NetworkResource{
   344  			{
   345  				Device: "eth0",
   346  				IP:     "192.168.0.100",
   347  				MBits:  50,
   348  				ReservedPorts: []structs.Port{
   349  					{Label: "ssh", Value: 22},
   350  					{Label: "other", Value: 1234},
   351  				},
   352  			},
   353  		},
   354  	}
   355  	task := a.Job.TaskGroups[0].Tasks[0]
   356  	task.Env = map[string]string{
   357  		"taskEnvKey":        "taskEnvVal",
   358  		"nested.task.key":   "x",
   359  		"invalid...taskkey": "y",
   360  		".a":                "a",
   361  		"b.":                "b",
   362  		".":                 "c",
   363  	}
   364  	env := NewBuilder(n, a, task, "global").SetDriverNetwork(
   365  		&drivers.DriverNetwork{PortMap: map[string]int{"https": 443}},
   366  	)
   367  
   368  	values, errs, err := env.Build().AllValues()
   369  	require.NoError(t, err)
   370  
   371  	// Assert the keys we couldn't nest were reported
   372  	require.Len(t, errs, 5)
   373  	require.Contains(t, errs, "invalid...taskkey")
   374  	require.Contains(t, errs, "meta.invalid...metakey")
   375  	require.Contains(t, errs, ".a")
   376  	require.Contains(t, errs, "b.")
   377  	require.Contains(t, errs, ".")
   378  
   379  	exp := map[string]string{
   380  		// Node
   381  		"node.unique.id":          n.ID,
   382  		"node.region":             "global",
   383  		"node.datacenter":         n.Datacenter,
   384  		"node.unique.name":        n.Name,
   385  		"node.class":              n.NodeClass,
   386  		"meta.metaKey":            "metaVal",
   387  		"attr.arch":               "x86",
   388  		"attr.driver.exec":        "1",
   389  		"attr.driver.mock_driver": "1",
   390  		"attr.kernel.name":        "linux",
   391  		"attr.nomad.version":      "0.5.0",
   392  
   393  		// 0.9 style meta and attr
   394  		"node.meta.metaKey":            "metaVal",
   395  		"node.attr.arch":               "x86",
   396  		"node.attr.driver.exec":        "1",
   397  		"node.attr.driver.mock_driver": "1",
   398  		"node.attr.kernel.name":        "linux",
   399  		"node.attr.nomad.version":      "0.5.0",
   400  
   401  		// Env
   402  		"taskEnvKey":                    "taskEnvVal",
   403  		"NOMAD_ADDR_http":               "127.0.0.1:80",
   404  		"NOMAD_PORT_http":               "80",
   405  		"NOMAD_IP_http":                 "127.0.0.1",
   406  		"NOMAD_ADDR_https":              "127.0.0.1:8080",
   407  		"NOMAD_PORT_https":              "443",
   408  		"NOMAD_IP_https":                "127.0.0.1",
   409  		"NOMAD_HOST_PORT_http":          "80",
   410  		"NOMAD_HOST_PORT_https":         "8080",
   411  		"NOMAD_TASK_NAME":               "web",
   412  		"NOMAD_GROUP_NAME":              "web",
   413  		"NOMAD_ADDR_ssh_other":          "192.168.0.100:1234",
   414  		"NOMAD_ADDR_ssh_ssh":            "192.168.0.100:22",
   415  		"NOMAD_IP_ssh_other":            "192.168.0.100",
   416  		"NOMAD_IP_ssh_ssh":              "192.168.0.100",
   417  		"NOMAD_PORT_ssh_other":          "1234",
   418  		"NOMAD_PORT_ssh_ssh":            "22",
   419  		"NOMAD_CPU_LIMIT":               "500",
   420  		"NOMAD_DC":                      "dc1",
   421  		"NOMAD_REGION":                  "global",
   422  		"NOMAD_MEMORY_LIMIT":            "256",
   423  		"NOMAD_META_ELB_CHECK_INTERVAL": "30s",
   424  		"NOMAD_META_ELB_CHECK_MIN":      "3",
   425  		"NOMAD_META_ELB_CHECK_TYPE":     "http",
   426  		"NOMAD_META_FOO":                "bar",
   427  		"NOMAD_META_OWNER":              "armon",
   428  		"NOMAD_META_elb_check_interval": "30s",
   429  		"NOMAD_META_elb_check_min":      "3",
   430  		"NOMAD_META_elb_check_type":     "http",
   431  		"NOMAD_META_foo":                "bar",
   432  		"NOMAD_META_owner":              "armon",
   433  		"NOMAD_JOB_NAME":                "my-job",
   434  		"NOMAD_ALLOC_ID":                a.ID,
   435  		"NOMAD_ALLOC_INDEX":             "0",
   436  
   437  		// 0.9 style env map
   438  		`env["taskEnvKey"]`:        "taskEnvVal",
   439  		`env["NOMAD_ADDR_http"]`:   "127.0.0.1:80",
   440  		`env["nested.task.key"]`:   "x",
   441  		`env["invalid...taskkey"]`: "y",
   442  		`env[".a"]`:                "a",
   443  		`env["b."]`:                "b",
   444  		`env["."]`:                 "c",
   445  	}
   446  
   447  	evalCtx := &hcl.EvalContext{
   448  		Variables: values,
   449  	}
   450  
   451  	for k, expectedVal := range exp {
   452  		t.Run(k, func(t *testing.T) {
   453  			// Parse HCL containing the test key
   454  			hclStr := fmt.Sprintf(`"${%s}"`, k)
   455  			expr, diag := hclsyntax.ParseExpression([]byte(hclStr), "test.hcl", hcl.Pos{})
   456  			require.Empty(t, diag)
   457  
   458  			// Decode with the TaskEnv values
   459  			out := ""
   460  			diag = gohcl.DecodeExpression(expr, evalCtx, &out)
   461  			require.Empty(t, diag)
   462  			require.Equal(t, out, expectedVal)
   463  		})
   464  	}
   465  }
   466  
   467  func TestEnvironment_VaultToken(t *testing.T) {
   468  	n := mock.Node()
   469  	a := mock.Alloc()
   470  	env := NewBuilder(n, a, a.Job.TaskGroups[0].Tasks[0], "global")
   471  	env.SetVaultToken("123", "vault-namespace", false)
   472  
   473  	{
   474  		act := env.Build().All()
   475  		if act[VaultToken] != "" {
   476  			t.Fatalf("Unexpected environment variables: %s=%q", VaultToken, act[VaultToken])
   477  		}
   478  		if act[VaultNamespace] != "" {
   479  			t.Fatalf("Unexpected environment variables: %s=%q", VaultNamespace, act[VaultNamespace])
   480  		}
   481  	}
   482  
   483  	{
   484  		act := env.SetVaultToken("123", "", true).Build().List()
   485  		exp := "VAULT_TOKEN=123"
   486  		found := false
   487  		foundNs := false
   488  		for _, entry := range act {
   489  			if entry == exp {
   490  				found = true
   491  			}
   492  			if strings.HasPrefix(entry, "VAULT_NAMESPACE=") {
   493  				foundNs = true
   494  			}
   495  		}
   496  		if !found {
   497  			t.Fatalf("did not find %q in:\n%s", exp, strings.Join(act, "\n"))
   498  		}
   499  		if foundNs {
   500  			t.Fatalf("found unwanted VAULT_NAMESPACE in:\n%s", strings.Join(act, "\n"))
   501  		}
   502  	}
   503  
   504  	{
   505  		act := env.SetVaultToken("123", "vault-namespace", true).Build().List()
   506  		exp := "VAULT_TOKEN=123"
   507  		expNs := "VAULT_NAMESPACE=vault-namespace"
   508  		found := false
   509  		foundNs := false
   510  		for _, entry := range act {
   511  			if entry == exp {
   512  				found = true
   513  			}
   514  			if entry == expNs {
   515  				foundNs = true
   516  			}
   517  		}
   518  		if !found {
   519  			t.Fatalf("did not find %q in:\n%s", exp, strings.Join(act, "\n"))
   520  		}
   521  		if !foundNs {
   522  			t.Fatalf("did not find %q in:\n%s", expNs, strings.Join(act, "\n"))
   523  		}
   524  	}
   525  }
   526  
   527  func TestEnvironment_Envvars(t *testing.T) {
   528  	envMap := map[string]string{"foo": "baz", "bar": "bang"}
   529  	n := mock.Node()
   530  	a := mock.Alloc()
   531  	task := a.Job.TaskGroups[0].Tasks[0]
   532  	task.Env = envMap
   533  	net := &drivers.DriverNetwork{PortMap: portMap}
   534  	act := NewBuilder(n, a, task, "global").SetDriverNetwork(net).Build().All()
   535  	for k, v := range envMap {
   536  		actV, ok := act[k]
   537  		if !ok {
   538  			t.Fatalf("missing %q in %#v", k, act)
   539  		}
   540  		if v != actV {
   541  			t.Fatalf("expected %s=%q but found %q", k, v, actV)
   542  		}
   543  	}
   544  }
   545  
   546  // TestEnvironment_HookVars asserts hook env vars are LWW and deletes of later
   547  // writes allow earlier hook's values to be visible.
   548  func TestEnvironment_HookVars(t *testing.T) {
   549  	n := mock.Node()
   550  	a := mock.Alloc()
   551  	builder := NewBuilder(n, a, a.Job.TaskGroups[0].Tasks[0], "global")
   552  
   553  	// Add vars from two hooks and assert the second one wins on
   554  	// conflicting keys.
   555  	builder.SetHookEnv("hookA", map[string]string{
   556  		"foo": "bar",
   557  		"baz": "quux",
   558  	})
   559  	builder.SetHookEnv("hookB", map[string]string{
   560  		"foo":   "123",
   561  		"hookB": "wins",
   562  	})
   563  
   564  	{
   565  		out := builder.Build().All()
   566  		assert.Equal(t, "123", out["foo"])
   567  		assert.Equal(t, "quux", out["baz"])
   568  		assert.Equal(t, "wins", out["hookB"])
   569  	}
   570  
   571  	// Asserting overwriting hook vars allows the first hooks original
   572  	// value to be used.
   573  	builder.SetHookEnv("hookB", nil)
   574  	{
   575  		out := builder.Build().All()
   576  		assert.Equal(t, "bar", out["foo"])
   577  		assert.Equal(t, "quux", out["baz"])
   578  		assert.NotContains(t, out, "hookB")
   579  	}
   580  }
   581  
   582  // TestEnvironment_DeviceHookVars asserts device hook env vars are accessible
   583  // separately.
   584  func TestEnvironment_DeviceHookVars(t *testing.T) {
   585  	require := require.New(t)
   586  	n := mock.Node()
   587  	a := mock.Alloc()
   588  	builder := NewBuilder(n, a, a.Job.TaskGroups[0].Tasks[0], "global")
   589  
   590  	// Add vars from two hooks and assert the second one wins on
   591  	// conflicting keys.
   592  	builder.SetHookEnv("hookA", map[string]string{
   593  		"foo": "bar",
   594  		"baz": "quux",
   595  	})
   596  	builder.SetDeviceHookEnv("devices", map[string]string{
   597  		"hook": "wins",
   598  	})
   599  
   600  	b := builder.Build()
   601  	deviceEnv := b.DeviceEnv()
   602  	require.Len(deviceEnv, 1)
   603  	require.Contains(deviceEnv, "hook")
   604  
   605  	all := b.Map()
   606  	require.Contains(all, "foo")
   607  }
   608  
   609  func TestEnvironment_Interpolate(t *testing.T) {
   610  	n := mock.Node()
   611  	n.Attributes["arch"] = "x86"
   612  	n.NodeClass = "test class"
   613  	a := mock.Alloc()
   614  	task := a.Job.TaskGroups[0].Tasks[0]
   615  	task.Env = map[string]string{"test": "${node.class}", "test2": "${attr.arch}"}
   616  	env := NewBuilder(n, a, task, "global").Build()
   617  
   618  	exp := []string{fmt.Sprintf("test=%s", n.NodeClass), fmt.Sprintf("test2=%s", n.Attributes["arch"])}
   619  	found1, found2 := false, false
   620  	for _, entry := range env.List() {
   621  		switch entry {
   622  		case exp[0]:
   623  			found1 = true
   624  		case exp[1]:
   625  			found2 = true
   626  		}
   627  	}
   628  	if !found1 || !found2 {
   629  		t.Fatalf("expected to find %q and %q but got:\n%s",
   630  			exp[0], exp[1], strings.Join(env.List(), "\n"))
   631  	}
   632  }
   633  
   634  func TestEnvironment_AppendHostEnvvars(t *testing.T) {
   635  	host := os.Environ()
   636  	if len(host) < 2 {
   637  		t.Skip("No host environment variables. Can't test")
   638  	}
   639  	skip := strings.Split(host[0], "=")[0]
   640  	env := testEnvBuilder().
   641  		SetHostEnvvars([]string{skip}).
   642  		Build()
   643  
   644  	act := env.Map()
   645  	if len(act) < 1 {
   646  		t.Fatalf("Host environment variables not properly set")
   647  	}
   648  	if _, ok := act[skip]; ok {
   649  		t.Fatalf("Didn't filter environment variable %q", skip)
   650  	}
   651  }
   652  
   653  // TestEnvironment_DashesInTaskName asserts dashes in port labels are properly
   654  // converted to underscores in environment variables.
   655  // See: https://github.com/hashicorp/nomad/issues/2405
   656  func TestEnvironment_DashesInTaskName(t *testing.T) {
   657  	a := mock.Alloc()
   658  	task := a.Job.TaskGroups[0].Tasks[0]
   659  	task.Env = map[string]string{"test-one-two": "three-four"}
   660  	envMap := NewBuilder(mock.Node(), a, task, "global").Build().Map()
   661  
   662  	if envMap["test_one_two"] != "three-four" {
   663  		t.Fatalf("Expected test_one_two=three-four in TaskEnv; found:\n%#v", envMap)
   664  	}
   665  }
   666  
   667  // TestEnvironment_UpdateTask asserts env vars and task meta are updated when a
   668  // task is updated.
   669  func TestEnvironment_UpdateTask(t *testing.T) {
   670  	a := mock.Alloc()
   671  	a.Job.TaskGroups[0].Meta = map[string]string{"tgmeta": "tgmetaval"}
   672  	task := a.Job.TaskGroups[0].Tasks[0]
   673  	task.Name = "orig"
   674  	task.Env = map[string]string{"env": "envval"}
   675  	task.Meta = map[string]string{"taskmeta": "taskmetaval"}
   676  	builder := NewBuilder(mock.Node(), a, task, "global")
   677  
   678  	origMap := builder.Build().Map()
   679  	if origMap["NOMAD_TASK_NAME"] != "orig" {
   680  		t.Errorf("Expected NOMAD_TASK_NAME=orig but found %q", origMap["NOMAD_TASK_NAME"])
   681  	}
   682  	if origMap["NOMAD_META_taskmeta"] != "taskmetaval" {
   683  		t.Errorf("Expected NOMAD_META_taskmeta=taskmetaval but found %q", origMap["NOMAD_META_taskmeta"])
   684  	}
   685  	if origMap["env"] != "envval" {
   686  		t.Errorf("Expected env=envva but found %q", origMap["env"])
   687  	}
   688  	if origMap["NOMAD_META_tgmeta"] != "tgmetaval" {
   689  		t.Errorf("Expected NOMAD_META_tgmeta=tgmetaval but found %q", origMap["NOMAD_META_tgmeta"])
   690  	}
   691  
   692  	a.Job.TaskGroups[0].Meta = map[string]string{"tgmeta2": "tgmetaval2"}
   693  	task.Name = "new"
   694  	task.Env = map[string]string{"env2": "envval2"}
   695  	task.Meta = map[string]string{"taskmeta2": "taskmetaval2"}
   696  
   697  	newMap := builder.UpdateTask(a, task).Build().Map()
   698  	if newMap["NOMAD_TASK_NAME"] != "new" {
   699  		t.Errorf("Expected NOMAD_TASK_NAME=new but found %q", newMap["NOMAD_TASK_NAME"])
   700  	}
   701  	if newMap["NOMAD_META_taskmeta2"] != "taskmetaval2" {
   702  		t.Errorf("Expected NOMAD_META_taskmeta=taskmetaval but found %q", newMap["NOMAD_META_taskmeta2"])
   703  	}
   704  	if newMap["env2"] != "envval2" {
   705  		t.Errorf("Expected env=envva but found %q", newMap["env2"])
   706  	}
   707  	if newMap["NOMAD_META_tgmeta2"] != "tgmetaval2" {
   708  		t.Errorf("Expected NOMAD_META_tgmeta=tgmetaval but found %q", newMap["NOMAD_META_tgmeta2"])
   709  	}
   710  	if v, ok := newMap["NOMAD_META_taskmeta"]; ok {
   711  		t.Errorf("Expected NOMAD_META_taskmeta to be unset but found: %q", v)
   712  	}
   713  }
   714  
   715  // TestEnvironment_InterpolateEmptyOptionalMeta asserts that in a parameterized
   716  // job, if an optional meta field is not set, it will get interpolated as an
   717  // empty string.
   718  func TestEnvironment_InterpolateEmptyOptionalMeta(t *testing.T) {
   719  	require := require.New(t)
   720  	a := mock.Alloc()
   721  	a.Job.ParameterizedJob = &structs.ParameterizedJobConfig{
   722  		MetaOptional: []string{"metaopt1", "metaopt2"},
   723  	}
   724  	a.Job.Dispatched = true
   725  	task := a.Job.TaskGroups[0].Tasks[0]
   726  	task.Meta = map[string]string{"metaopt1": "metaopt1val"}
   727  	env := NewBuilder(mock.Node(), a, task, "global").Build()
   728  	require.Equal("metaopt1val", env.ReplaceEnv("${NOMAD_META_metaopt1}"))
   729  	require.Empty(env.ReplaceEnv("${NOMAD_META_metaopt2}"))
   730  }