github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/helper/pluginutils/hclutils/util_test.go (about)

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