github.com/hernad/nomad@v1.6.112/helper/pluginutils/hclutils/util_test.go (about)

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