github.com/tonyhb/nomad@v0.11.8/helper/pluginutils/hclutils/util_test.go (about)

     1  package hclutils_test
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/hashicorp/hcl2/hcldec"
     7  	"github.com/hashicorp/nomad/drivers/docker"
     8  	"github.com/hashicorp/nomad/helper/pluginutils/hclspecutils"
     9  	"github.com/hashicorp/nomad/helper/pluginutils/hclutils"
    10  	"github.com/hashicorp/nomad/plugins/drivers"
    11  	"github.com/hashicorp/nomad/plugins/shared/hclspec"
    12  	"github.com/kr/pretty"
    13  	"github.com/stretchr/testify/require"
    14  	"github.com/zclconf/go-cty/cty"
    15  )
    16  
    17  func TestParseHclInterface_Hcl(t *testing.T) {
    18  	dockerDriver := new(docker.Driver)
    19  	dockerSpec, err := dockerDriver.TaskConfigSchema()
    20  	require.NoError(t, err)
    21  	dockerDecSpec, diags := hclspecutils.Convert(dockerSpec)
    22  	require.False(t, diags.HasErrors())
    23  
    24  	vars := map[string]cty.Value{
    25  		"NOMAD_ALLOC_INDEX": cty.NumberIntVal(2),
    26  		"NOMAD_META_hello":  cty.StringVal("world"),
    27  	}
    28  
    29  	cases := []struct {
    30  		name         string
    31  		config       interface{}
    32  		spec         hcldec.Spec
    33  		vars         map[string]cty.Value
    34  		expected     interface{}
    35  		expectedType interface{}
    36  	}{
    37  		{
    38  			name: "single string attr",
    39  			config: hclutils.HclConfigToInterface(t, `
    40  			config {
    41  				image = "redis:3.2"
    42  			}`),
    43  			spec: dockerDecSpec,
    44  			expected: &docker.TaskConfig{
    45  				Image:        "redis:3.2",
    46  				Devices:      []docker.DockerDevice{},
    47  				Mounts:       []docker.DockerMount{},
    48  				CPUCFSPeriod: 100000,
    49  			},
    50  			expectedType: &docker.TaskConfig{},
    51  		},
    52  		{
    53  			name: "single string attr json",
    54  			config: hclutils.JsonConfigToInterface(t, `
    55  						{
    56  							"Config": {
    57  								"image": "redis:3.2"
    58  			                }
    59  						}`),
    60  			spec: dockerDecSpec,
    61  			expected: &docker.TaskConfig{
    62  				Image:        "redis:3.2",
    63  				Devices:      []docker.DockerDevice{},
    64  				Mounts:       []docker.DockerMount{},
    65  				CPUCFSPeriod: 100000,
    66  			},
    67  			expectedType: &docker.TaskConfig{},
    68  		},
    69  		{
    70  			name: "number attr",
    71  			config: hclutils.HclConfigToInterface(t, `
    72  						config {
    73  							image = "redis:3.2"
    74  							pids_limit  = 2
    75  						}`),
    76  			spec: dockerDecSpec,
    77  			expected: &docker.TaskConfig{
    78  				Image:        "redis:3.2",
    79  				PidsLimit:    2,
    80  				Devices:      []docker.DockerDevice{},
    81  				Mounts:       []docker.DockerMount{},
    82  				CPUCFSPeriod: 100000,
    83  			},
    84  			expectedType: &docker.TaskConfig{},
    85  		},
    86  		{
    87  			name: "number attr json",
    88  			config: hclutils.JsonConfigToInterface(t, `
    89  						{
    90  							"Config": {
    91  								"image": "redis:3.2",
    92  								"pids_limit": "2"
    93  			                }
    94  						}`),
    95  			spec: dockerDecSpec,
    96  			expected: &docker.TaskConfig{
    97  				Image:        "redis:3.2",
    98  				PidsLimit:    2,
    99  				Devices:      []docker.DockerDevice{},
   100  				Mounts:       []docker.DockerMount{},
   101  				CPUCFSPeriod: 100000,
   102  			},
   103  			expectedType: &docker.TaskConfig{},
   104  		},
   105  		{
   106  			name: "number attr interpolated",
   107  			config: hclutils.HclConfigToInterface(t, `
   108  						config {
   109  							image = "redis:3.2"
   110  							pids_limit  = "${2 + 2}"
   111  						}`),
   112  			spec: dockerDecSpec,
   113  			expected: &docker.TaskConfig{
   114  				Image:        "redis:3.2",
   115  				PidsLimit:    4,
   116  				Devices:      []docker.DockerDevice{},
   117  				Mounts:       []docker.DockerMount{},
   118  				CPUCFSPeriod: 100000,
   119  			},
   120  			expectedType: &docker.TaskConfig{},
   121  		},
   122  		{
   123  			name: "number attr interploated json",
   124  			config: hclutils.JsonConfigToInterface(t, `
   125  						{
   126  							"Config": {
   127  								"image": "redis:3.2",
   128  								"pids_limit": "${2 + 2}"
   129  			                }
   130  						}`),
   131  			spec: dockerDecSpec,
   132  			expected: &docker.TaskConfig{
   133  				Image:        "redis:3.2",
   134  				PidsLimit:    4,
   135  				Devices:      []docker.DockerDevice{},
   136  				Mounts:       []docker.DockerMount{},
   137  				CPUCFSPeriod: 100000,
   138  			},
   139  			expectedType: &docker.TaskConfig{},
   140  		},
   141  		{
   142  			name: "multi attr",
   143  			config: hclutils.HclConfigToInterface(t, `
   144  						config {
   145  							image = "redis:3.2"
   146  							args = ["foo", "bar"]
   147  						}`),
   148  			spec: dockerDecSpec,
   149  			expected: &docker.TaskConfig{
   150  				Image:        "redis:3.2",
   151  				Args:         []string{"foo", "bar"},
   152  				Devices:      []docker.DockerDevice{},
   153  				Mounts:       []docker.DockerMount{},
   154  				CPUCFSPeriod: 100000,
   155  			},
   156  			expectedType: &docker.TaskConfig{},
   157  		},
   158  		{
   159  			name: "multi attr json",
   160  			config: hclutils.JsonConfigToInterface(t, `
   161  						{
   162  							"Config": {
   163  								"image": "redis:3.2",
   164  								"args": ["foo", "bar"]
   165  			                }
   166  						}`),
   167  			spec: dockerDecSpec,
   168  			expected: &docker.TaskConfig{
   169  				Image:        "redis:3.2",
   170  				Args:         []string{"foo", "bar"},
   171  				Devices:      []docker.DockerDevice{},
   172  				Mounts:       []docker.DockerMount{},
   173  				CPUCFSPeriod: 100000,
   174  			},
   175  			expectedType: &docker.TaskConfig{},
   176  		},
   177  		{
   178  			name: "multi attr variables",
   179  			config: hclutils.HclConfigToInterface(t, `
   180  						config {
   181  							image = "redis:3.2"
   182  							args = ["${NOMAD_META_hello}", "${NOMAD_ALLOC_INDEX}"]
   183  							pids_limit = "${NOMAD_ALLOC_INDEX + 2}"
   184  						}`),
   185  			spec: dockerDecSpec,
   186  			vars: vars,
   187  			expected: &docker.TaskConfig{
   188  				Image:        "redis:3.2",
   189  				Args:         []string{"world", "2"},
   190  				PidsLimit:    4,
   191  				Devices:      []docker.DockerDevice{},
   192  				Mounts:       []docker.DockerMount{},
   193  				CPUCFSPeriod: 100000,
   194  			},
   195  			expectedType: &docker.TaskConfig{},
   196  		},
   197  		{
   198  			name: "multi attr variables json",
   199  			config: hclutils.JsonConfigToInterface(t, `
   200  						{
   201  							"Config": {
   202  								"image": "redis:3.2",
   203  								"args": ["foo", "bar"]
   204  			                }
   205  						}`),
   206  			spec: dockerDecSpec,
   207  			expected: &docker.TaskConfig{
   208  				Image:        "redis:3.2",
   209  				Args:         []string{"foo", "bar"},
   210  				Devices:      []docker.DockerDevice{},
   211  				Mounts:       []docker.DockerMount{},
   212  				CPUCFSPeriod: 100000,
   213  			},
   214  			expectedType: &docker.TaskConfig{},
   215  		},
   216  		{
   217  			name: "port_map",
   218  			config: hclutils.HclConfigToInterface(t, `
   219  			config {
   220  				image = "redis:3.2"
   221  				port_map {
   222  					foo = 1234
   223  					bar = 5678
   224  				}
   225  			}`),
   226  			spec: dockerDecSpec,
   227  			expected: &docker.TaskConfig{
   228  				Image: "redis:3.2",
   229  				PortMap: map[string]int{
   230  					"foo": 1234,
   231  					"bar": 5678,
   232  				},
   233  				Devices:      []docker.DockerDevice{},
   234  				Mounts:       []docker.DockerMount{},
   235  				CPUCFSPeriod: 100000,
   236  			},
   237  			expectedType: &docker.TaskConfig{},
   238  		},
   239  		{
   240  			name: "port_map json",
   241  			config: hclutils.JsonConfigToInterface(t, `
   242  							{
   243  								"Config": {
   244  									"image": "redis:3.2",
   245  									"port_map": [{
   246  										"foo": 1234,
   247  										"bar": 5678
   248  									}]
   249  				                }
   250  							}`),
   251  			spec: dockerDecSpec,
   252  			expected: &docker.TaskConfig{
   253  				Image: "redis:3.2",
   254  				PortMap: map[string]int{
   255  					"foo": 1234,
   256  					"bar": 5678,
   257  				},
   258  				Devices:      []docker.DockerDevice{},
   259  				Mounts:       []docker.DockerMount{},
   260  				CPUCFSPeriod: 100000,
   261  			},
   262  			expectedType: &docker.TaskConfig{},
   263  		},
   264  		{
   265  			name: "devices",
   266  			config: hclutils.HclConfigToInterface(t, `
   267  						config {
   268  							image = "redis:3.2"
   269  							devices = [
   270  								{
   271  									host_path = "/dev/sda1"
   272  									container_path = "/dev/xvdc"
   273  									cgroup_permissions = "r"
   274  								},
   275  								{
   276  									host_path = "/dev/sda2"
   277  									container_path = "/dev/xvdd"
   278  								}
   279  							]
   280  						}`),
   281  			spec: dockerDecSpec,
   282  			expected: &docker.TaskConfig{
   283  				Image: "redis:3.2",
   284  				Devices: []docker.DockerDevice{
   285  					{
   286  						HostPath:          "/dev/sda1",
   287  						ContainerPath:     "/dev/xvdc",
   288  						CgroupPermissions: "r",
   289  					},
   290  					{
   291  						HostPath:      "/dev/sda2",
   292  						ContainerPath: "/dev/xvdd",
   293  					},
   294  				},
   295  				Mounts:       []docker.DockerMount{},
   296  				CPUCFSPeriod: 100000,
   297  			},
   298  			expectedType: &docker.TaskConfig{},
   299  		},
   300  		{
   301  			name: "docker_logging",
   302  			config: hclutils.HclConfigToInterface(t, `
   303  				config {
   304  					image = "redis:3.2"
   305  					network_mode = "host"
   306  					dns_servers = ["169.254.1.1"]
   307  					logging {
   308  					    type = "syslog"
   309  					    config {
   310  						tag  = "driver-test"
   311  					    }
   312  					}
   313  				}`),
   314  			spec: dockerDecSpec,
   315  			expected: &docker.TaskConfig{
   316  				Image:       "redis:3.2",
   317  				NetworkMode: "host",
   318  				DNSServers:  []string{"169.254.1.1"},
   319  				Logging: docker.DockerLogging{
   320  					Type: "syslog",
   321  					Config: map[string]string{
   322  						"tag": "driver-test",
   323  					},
   324  				},
   325  				Devices:      []docker.DockerDevice{},
   326  				Mounts:       []docker.DockerMount{},
   327  				CPUCFSPeriod: 100000,
   328  			},
   329  			expectedType: &docker.TaskConfig{},
   330  		},
   331  		{
   332  			name: "docker_json",
   333  			config: hclutils.JsonConfigToInterface(t, `
   334  					{
   335  						"Config": {
   336  							"image": "redis:3.2",
   337  							"devices": [
   338  								{
   339  									"host_path": "/dev/sda1",
   340  									"container_path": "/dev/xvdc",
   341  									"cgroup_permissions": "r"
   342  								},
   343  								{
   344  									"host_path": "/dev/sda2",
   345  									"container_path": "/dev/xvdd"
   346  								}
   347  							]
   348  				}
   349  					}`),
   350  			spec: dockerDecSpec,
   351  			expected: &docker.TaskConfig{
   352  				Image: "redis:3.2",
   353  				Devices: []docker.DockerDevice{
   354  					{
   355  						HostPath:          "/dev/sda1",
   356  						ContainerPath:     "/dev/xvdc",
   357  						CgroupPermissions: "r",
   358  					},
   359  					{
   360  						HostPath:      "/dev/sda2",
   361  						ContainerPath: "/dev/xvdd",
   362  					},
   363  				},
   364  				Mounts:       []docker.DockerMount{},
   365  				CPUCFSPeriod: 100000,
   366  			},
   367  			expectedType: &docker.TaskConfig{},
   368  		},
   369  	}
   370  
   371  	for _, c := range cases {
   372  		c := c
   373  		t.Run(c.name, func(t *testing.T) {
   374  			t.Logf("Val: % #v", pretty.Formatter(c.config))
   375  			// Parse the interface
   376  			ctyValue, diag, errs := hclutils.ParseHclInterface(c.config, c.spec, c.vars)
   377  			if diag.HasErrors() {
   378  				for _, err := range errs {
   379  					t.Error(err)
   380  				}
   381  				t.FailNow()
   382  			}
   383  
   384  			// Test encoding
   385  			taskConfig := &drivers.TaskConfig{}
   386  			require.NoError(t, taskConfig.EncodeDriverConfig(ctyValue))
   387  
   388  			// Test decoding
   389  			require.NoError(t, taskConfig.DecodeDriverConfig(c.expectedType))
   390  
   391  			require.EqualValues(t, c.expected, c.expectedType)
   392  
   393  		})
   394  	}
   395  }
   396  
   397  func TestParseNullFields(t *testing.T) {
   398  	spec := hclspec.NewObject(map[string]*hclspec.Spec{
   399  		"array_field":   hclspec.NewAttr("array_field", "list(string)", false),
   400  		"string_field":  hclspec.NewAttr("string_field", "string", false),
   401  		"boolean_field": hclspec.NewAttr("boolean_field", "bool", false),
   402  		"number_field":  hclspec.NewAttr("number_field", "number", false),
   403  		"block_field": hclspec.NewBlock("block_field", false, hclspec.NewObject((map[string]*hclspec.Spec{
   404  			"f": hclspec.NewAttr("f", "string", true),
   405  		}))),
   406  		"block_list_field": hclspec.NewBlockList("block_list_field", hclspec.NewObject((map[string]*hclspec.Spec{
   407  			"f": hclspec.NewAttr("f", "string", true),
   408  		}))),
   409  	})
   410  
   411  	type Sub struct {
   412  		F string `codec:"f"`
   413  	}
   414  
   415  	type TaskConfig struct {
   416  		Array     []string `codec:"array_field"`
   417  		String    string   `codec:"string_field"`
   418  		Boolean   bool     `codec:"boolean_field"`
   419  		Number    int64    `codec:"number_field"`
   420  		Block     Sub      `codec:"block_field"`
   421  		BlockList []Sub    `codec:"block_list_field"`
   422  	}
   423  
   424  	cases := []struct {
   425  		name     string
   426  		json     string
   427  		expected TaskConfig
   428  	}{
   429  		{
   430  			"omitted fields",
   431  			`{"Config": {}}`,
   432  			TaskConfig{BlockList: []Sub{}},
   433  		},
   434  		{
   435  			"explicitly nil",
   436  			`{"Config": {
   437                              "array_field": null,
   438                              "string_field": null,
   439  			    "boolean_field": null,
   440                              "number_field": null,
   441                              "block_field": null,
   442                              "block_list_field": null}}`,
   443  			TaskConfig{BlockList: []Sub{}},
   444  		},
   445  		{
   446  			// for sanity checking that the fields are actually set
   447  			"explicitly set to not null",
   448  			`{"Config": {
   449                              "array_field": ["a"],
   450                              "string_field": "a",
   451                              "boolean_field": true,
   452                              "number_field": 5,
   453                              "block_field": [{"f": "a"}],
   454                              "block_list_field": [{"f": "a"}, {"f": "b"}]}}`,
   455  			TaskConfig{
   456  				Array:     []string{"a"},
   457  				String:    "a",
   458  				Boolean:   true,
   459  				Number:    5,
   460  				Block:     Sub{"a"},
   461  				BlockList: []Sub{{"a"}, {"b"}},
   462  			},
   463  		},
   464  	}
   465  
   466  	parser := hclutils.NewConfigParser(spec)
   467  	for _, c := range cases {
   468  		t.Run(c.name, func(t *testing.T) {
   469  			var tc TaskConfig
   470  			parser.ParseJson(t, c.json, &tc)
   471  
   472  			require.EqualValues(t, c.expected, tc)
   473  		})
   474  	}
   475  }
   476  
   477  func TestParseUnknown(t *testing.T) {
   478  	spec := hclspec.NewObject(map[string]*hclspec.Spec{
   479  		"string_field":   hclspec.NewAttr("string_field", "string", false),
   480  		"map_field":      hclspec.NewAttr("map_field", "map(string)", false),
   481  		"list_field":     hclspec.NewAttr("list_field", "map(string)", false),
   482  		"map_list_field": hclspec.NewAttr("map_list_field", "list(map(string))", false),
   483  	})
   484  	cSpec, diags := hclspecutils.Convert(spec)
   485  	require.False(t, diags.HasErrors())
   486  
   487  	cases := []struct {
   488  		name string
   489  		hcl  string
   490  	}{
   491  		{
   492  			"string field",
   493  			`config {  string_field = "${MYENV}" }`,
   494  		},
   495  		{
   496  			"map_field",
   497  			`config { map_field { key = "${MYENV}" }}`,
   498  		},
   499  		{
   500  			"list_field",
   501  			`config { list_field = ["${MYENV}"]}`,
   502  		},
   503  		{
   504  			"map_list_field",
   505  			`config { map_list_field { key = "${MYENV}"}}`,
   506  		},
   507  	}
   508  
   509  	vars := map[string]cty.Value{}
   510  
   511  	for _, c := range cases {
   512  		t.Run(c.name, func(t *testing.T) {
   513  			inter := hclutils.HclConfigToInterface(t, c.hcl)
   514  
   515  			ctyValue, diag, errs := hclutils.ParseHclInterface(inter, cSpec, vars)
   516  			t.Logf("parsed: %# v", pretty.Formatter(ctyValue))
   517  
   518  			require.NotNil(t, errs)
   519  			require.True(t, diag.HasErrors())
   520  			require.Contains(t, errs[0].Error(), "no variable named")
   521  		})
   522  	}
   523  }
   524  
   525  func TestParseInvalid(t *testing.T) {
   526  	dockerDriver := new(docker.Driver)
   527  	dockerSpec, err := dockerDriver.TaskConfigSchema()
   528  	require.NoError(t, err)
   529  	spec, diags := hclspecutils.Convert(dockerSpec)
   530  	require.False(t, diags.HasErrors())
   531  
   532  	cases := []struct {
   533  		name string
   534  		hcl  string
   535  	}{
   536  		{
   537  			"invalid_field",
   538  			`config { image = "redis:3.2" bad_key = "whatever"}`,
   539  		},
   540  	}
   541  
   542  	vars := map[string]cty.Value{}
   543  
   544  	for _, c := range cases {
   545  		t.Run(c.name, func(t *testing.T) {
   546  			inter := hclutils.HclConfigToInterface(t, c.hcl)
   547  
   548  			ctyValue, diag, errs := hclutils.ParseHclInterface(inter, spec, vars)
   549  			t.Logf("parsed: %# v", pretty.Formatter(ctyValue))
   550  
   551  			require.NotNil(t, errs)
   552  			require.True(t, diag.HasErrors())
   553  			require.Contains(t, errs[0].Error(), "Invalid label")
   554  		})
   555  	}
   556  }