github.com/zoomfoo/nomad@v0.8.5-0.20180907175415-f28fd3a1a056/plugins/shared/util_test.go (about)

     1  package shared
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/hashicorp/hcl"
     7  	"github.com/hashicorp/hcl/hcl/ast"
     8  	hcl2 "github.com/hashicorp/hcl2/hcl"
     9  	"github.com/hashicorp/hcl2/hcldec"
    10  	"github.com/hashicorp/nomad/helper"
    11  	"github.com/hashicorp/nomad/nomad/structs"
    12  	"github.com/kr/pretty"
    13  	"github.com/mitchellh/mapstructure"
    14  	"github.com/stretchr/testify/require"
    15  	"github.com/ugorji/go/codec"
    16  	"github.com/zclconf/go-cty/cty"
    17  	"github.com/zclconf/go-cty/cty/gocty"
    18  )
    19  
    20  var (
    21  	dockerSpec hcldec.Spec = hcldec.ObjectSpec(map[string]hcldec.Spec{
    22  		"image": &hcldec.AttrSpec{
    23  			Name:     "image",
    24  			Type:     cty.String,
    25  			Required: true,
    26  		},
    27  		"args": &hcldec.AttrSpec{
    28  			Name: "args",
    29  			Type: cty.List(cty.String),
    30  		},
    31  		"pids_limit": &hcldec.AttrSpec{
    32  			Name: "pids_limit",
    33  			Type: cty.Number,
    34  		},
    35  		"port_map": &hcldec.BlockAttrsSpec{
    36  			TypeName:    "port_map",
    37  			ElementType: cty.String,
    38  		},
    39  
    40  		"devices": &hcldec.BlockListSpec{
    41  			TypeName: "devices",
    42  			Nested: hcldec.ObjectSpec(map[string]hcldec.Spec{
    43  				"host_path": &hcldec.AttrSpec{
    44  					Name: "host_path",
    45  					Type: cty.String,
    46  				},
    47  				"container_path": &hcldec.AttrSpec{
    48  					Name: "container_path",
    49  					Type: cty.String,
    50  				},
    51  				"cgroup_permissions": &hcldec.DefaultSpec{
    52  					Primary: &hcldec.AttrSpec{
    53  						Name: "cgroup_permissions",
    54  						Type: cty.String,
    55  					},
    56  					Default: &hcldec.LiteralSpec{
    57  						Value: cty.StringVal(""),
    58  					},
    59  				},
    60  			}),
    61  		},
    62  	},
    63  	)
    64  )
    65  
    66  type dockerConfig struct {
    67  	Image     string            `cty:"image"`
    68  	Args      []string          `cty:"args"`
    69  	PidsLimit *int64            `cty:"pids_limit"`
    70  	PortMap   map[string]string `cty:"port_map"`
    71  	Devices   []DockerDevice    `cty:"devices"`
    72  }
    73  
    74  type DockerDevice struct {
    75  	HostPath          string `cty:"host_path"`
    76  	ContainerPath     string `cty:"container_path"`
    77  	CgroupPermissions string `cty:"cgroup_permissions"`
    78  }
    79  
    80  func hclConfigToInterface(t *testing.T, config string) interface{} {
    81  	t.Helper()
    82  
    83  	// Parse as we do in the jobspec parser
    84  	root, err := hcl.Parse(config)
    85  	if err != nil {
    86  		t.Fatalf("failed to hcl parse the config: %v", err)
    87  	}
    88  
    89  	// Top-level item should be a list
    90  	list, ok := root.Node.(*ast.ObjectList)
    91  	if !ok {
    92  		t.Fatalf("root should be an object")
    93  	}
    94  
    95  	var m map[string]interface{}
    96  	if err := hcl.DecodeObject(&m, list.Items[0]); err != nil {
    97  		t.Fatalf("failed to decode object: %v", err)
    98  	}
    99  
   100  	var m2 map[string]interface{}
   101  	if err := mapstructure.WeakDecode(m, &m2); err != nil {
   102  		t.Fatalf("failed to weak decode object: %v", err)
   103  	}
   104  
   105  	return m2["config"]
   106  }
   107  
   108  func jsonConfigToInterface(t *testing.T, config string) interface{} {
   109  	t.Helper()
   110  
   111  	// Decode from json
   112  	dec := codec.NewDecoderBytes([]byte(config), structs.JsonHandle)
   113  
   114  	var m map[string]interface{}
   115  	err := dec.Decode(&m)
   116  	if err != nil {
   117  		t.Fatalf("failed to decode: %v", err)
   118  	}
   119  
   120  	return m["Config"]
   121  }
   122  
   123  func TestParseHclInterface_Hcl(t *testing.T) {
   124  	defaultCtx := &hcl2.EvalContext{
   125  		Functions: GetStdlibFuncs(),
   126  	}
   127  	variableCtx := &hcl2.EvalContext{
   128  		Functions: GetStdlibFuncs(),
   129  		Variables: map[string]cty.Value{
   130  			"NOMAD_ALLOC_INDEX": cty.NumberIntVal(2),
   131  			"NOMAD_META_hello":  cty.StringVal("world"),
   132  		},
   133  	}
   134  
   135  	// XXX Useful for determining what cty thinks the type is
   136  	//implied, err := gocty.ImpliedType(&dockerConfig{})
   137  	//if err != nil {
   138  	//t.Fatalf("implied type failed: %v", err)
   139  	//}
   140  
   141  	//t.Logf("Implied type: %v", implied.GoString())
   142  
   143  	cases := []struct {
   144  		name         string
   145  		config       interface{}
   146  		spec         hcldec.Spec
   147  		ctx          *hcl2.EvalContext
   148  		expected     interface{}
   149  		expectedType interface{}
   150  	}{
   151  		{
   152  			name: "single string attr",
   153  			config: hclConfigToInterface(t, `
   154  			config {
   155  				image = "redis:3.2"
   156  			}`),
   157  			spec: dockerSpec,
   158  			ctx:  defaultCtx,
   159  			expected: &dockerConfig{
   160  				Image:   "redis:3.2",
   161  				Devices: []DockerDevice{},
   162  			},
   163  			expectedType: &dockerConfig{},
   164  		},
   165  		{
   166  			name: "single string attr json",
   167  			config: jsonConfigToInterface(t, `
   168  						{
   169  							"Config": {
   170  								"image": "redis:3.2"
   171  			                }
   172  						}`),
   173  			spec: dockerSpec,
   174  			ctx:  defaultCtx,
   175  			expected: &dockerConfig{
   176  				Image:   "redis:3.2",
   177  				Devices: []DockerDevice{},
   178  			},
   179  			expectedType: &dockerConfig{},
   180  		},
   181  		{
   182  			name: "number attr",
   183  			config: hclConfigToInterface(t, `
   184  						config {
   185  							image = "redis:3.2"
   186  							pids_limit  = 2
   187  						}`),
   188  			spec: dockerSpec,
   189  			ctx:  defaultCtx,
   190  			expected: &dockerConfig{
   191  				Image:     "redis:3.2",
   192  				PidsLimit: helper.Int64ToPtr(2),
   193  				Devices:   []DockerDevice{},
   194  			},
   195  			expectedType: &dockerConfig{},
   196  		},
   197  		{
   198  			name: "number attr json",
   199  			config: jsonConfigToInterface(t, `
   200  						{
   201  							"Config": {
   202  								"image": "redis:3.2",
   203  								"pids_limit": "2"
   204  			                }
   205  						}`),
   206  			spec: dockerSpec,
   207  			ctx:  defaultCtx,
   208  			expected: &dockerConfig{
   209  				Image:     "redis:3.2",
   210  				PidsLimit: helper.Int64ToPtr(2),
   211  				Devices:   []DockerDevice{},
   212  			},
   213  			expectedType: &dockerConfig{},
   214  		},
   215  		{
   216  			name: "number attr interpolated",
   217  			config: hclConfigToInterface(t, `
   218  						config {
   219  							image = "redis:3.2"
   220  							pids_limit  = "${2 + 2}"
   221  						}`),
   222  			spec: dockerSpec,
   223  			ctx:  defaultCtx,
   224  			expected: &dockerConfig{
   225  				Image:     "redis:3.2",
   226  				PidsLimit: helper.Int64ToPtr(4),
   227  				Devices:   []DockerDevice{},
   228  			},
   229  			expectedType: &dockerConfig{},
   230  		},
   231  		{
   232  			name: "number attr interploated json",
   233  			config: jsonConfigToInterface(t, `
   234  						{
   235  							"Config": {
   236  								"image": "redis:3.2",
   237  								"pids_limit": "${2 + 2}"
   238  			                }
   239  						}`),
   240  			spec: dockerSpec,
   241  			ctx:  defaultCtx,
   242  			expected: &dockerConfig{
   243  				Image:     "redis:3.2",
   244  				PidsLimit: helper.Int64ToPtr(4),
   245  				Devices:   []DockerDevice{},
   246  			},
   247  			expectedType: &dockerConfig{},
   248  		},
   249  		{
   250  			name: "multi attr",
   251  			config: hclConfigToInterface(t, `
   252  						config {
   253  							image = "redis:3.2"
   254  							args = ["foo", "bar"]
   255  						}`),
   256  			spec: dockerSpec,
   257  			ctx:  defaultCtx,
   258  			expected: &dockerConfig{
   259  				Image:   "redis:3.2",
   260  				Args:    []string{"foo", "bar"},
   261  				Devices: []DockerDevice{},
   262  			},
   263  			expectedType: &dockerConfig{},
   264  		},
   265  		{
   266  			name: "multi attr json",
   267  			config: jsonConfigToInterface(t, `
   268  						{
   269  							"Config": {
   270  								"image": "redis:3.2",
   271  								"args": ["foo", "bar"]
   272  			                }
   273  						}`),
   274  			spec: dockerSpec,
   275  			ctx:  defaultCtx,
   276  			expected: &dockerConfig{
   277  				Image:   "redis:3.2",
   278  				Args:    []string{"foo", "bar"},
   279  				Devices: []DockerDevice{},
   280  			},
   281  			expectedType: &dockerConfig{},
   282  		},
   283  		{
   284  			name: "multi attr variables",
   285  			config: hclConfigToInterface(t, `
   286  						config {
   287  							image = "redis:3.2"
   288  							args = ["${NOMAD_META_hello}", "${NOMAD_ALLOC_INDEX}"]
   289  							pids_limit = "${NOMAD_ALLOC_INDEX + 2}"
   290  						}`),
   291  			spec: dockerSpec,
   292  			ctx:  variableCtx,
   293  			expected: &dockerConfig{
   294  				Image:     "redis:3.2",
   295  				Args:      []string{"world", "2"},
   296  				PidsLimit: helper.Int64ToPtr(4),
   297  				Devices:   []DockerDevice{},
   298  			},
   299  			expectedType: &dockerConfig{},
   300  		},
   301  		{
   302  			name: "multi attr variables json",
   303  			config: jsonConfigToInterface(t, `
   304  						{
   305  							"Config": {
   306  								"image": "redis:3.2",
   307  								"args": ["foo", "bar"]
   308  			                }
   309  						}`),
   310  			spec: dockerSpec,
   311  			ctx:  defaultCtx,
   312  			expected: &dockerConfig{
   313  				Image:   "redis:3.2",
   314  				Args:    []string{"foo", "bar"},
   315  				Devices: []DockerDevice{},
   316  			},
   317  			expectedType: &dockerConfig{},
   318  		},
   319  		{
   320  			name: "port_map",
   321  			config: hclConfigToInterface(t, `
   322  			config {
   323  				image = "redis:3.2"
   324  				port_map {
   325  					foo = "db"
   326  					bar = "db2"
   327  				}
   328  			}`),
   329  			spec: dockerSpec,
   330  			ctx:  defaultCtx,
   331  			expected: &dockerConfig{
   332  				Image: "redis:3.2",
   333  				PortMap: map[string]string{
   334  					"foo": "db",
   335  					"bar": "db2",
   336  				},
   337  				Devices: []DockerDevice{},
   338  			},
   339  			expectedType: &dockerConfig{},
   340  		},
   341  		{
   342  			name: "port_map json",
   343  			config: jsonConfigToInterface(t, `
   344  							{
   345  								"Config": {
   346  									"image": "redis:3.2",
   347  									"port_map": [{
   348  										"foo": "db",
   349  										"bar": "db2"
   350  									}]
   351  				                }
   352  							}`),
   353  			spec: dockerSpec,
   354  			ctx:  defaultCtx,
   355  			expected: &dockerConfig{
   356  				Image: "redis:3.2",
   357  				PortMap: map[string]string{
   358  					"foo": "db",
   359  					"bar": "db2",
   360  				},
   361  				Devices: []DockerDevice{},
   362  			},
   363  			expectedType: &dockerConfig{},
   364  		},
   365  		{
   366  			name: "devices",
   367  			config: hclConfigToInterface(t, `
   368  						config {
   369  							image = "redis:3.2"
   370  							devices = [
   371  								{
   372  									host_path = "/dev/sda1"
   373  									container_path = "/dev/xvdc"
   374  									cgroup_permissions = "r"
   375  								},
   376  								{
   377  									host_path = "/dev/sda2"
   378  									container_path = "/dev/xvdd"
   379  								}
   380  							]
   381  						}`),
   382  			spec: dockerSpec,
   383  			ctx:  defaultCtx,
   384  			expected: &dockerConfig{
   385  				Image: "redis:3.2",
   386  				Devices: []DockerDevice{
   387  					{
   388  						HostPath:          "/dev/sda1",
   389  						ContainerPath:     "/dev/xvdc",
   390  						CgroupPermissions: "r",
   391  					},
   392  					{
   393  						HostPath:      "/dev/sda2",
   394  						ContainerPath: "/dev/xvdd",
   395  					},
   396  				},
   397  			},
   398  			expectedType: &dockerConfig{},
   399  		},
   400  		{
   401  			name: "devices json",
   402  			config: jsonConfigToInterface(t, `
   403  							{
   404  								"Config": {
   405  									"image": "redis:3.2",
   406  									"devices": [
   407  										{
   408  											"host_path": "/dev/sda1",
   409  											"container_path": "/dev/xvdc",
   410  											"cgroup_permissions": "r"
   411  										},
   412  										{
   413  											"host_path": "/dev/sda2",
   414  											"container_path": "/dev/xvdd"
   415  										}
   416  									]
   417  				                }
   418  							}`),
   419  			spec: dockerSpec,
   420  			ctx:  defaultCtx,
   421  			expected: &dockerConfig{
   422  				Image: "redis:3.2",
   423  				Devices: []DockerDevice{
   424  					{
   425  						HostPath:          "/dev/sda1",
   426  						ContainerPath:     "/dev/xvdc",
   427  						CgroupPermissions: "r",
   428  					},
   429  					{
   430  						HostPath:      "/dev/sda2",
   431  						ContainerPath: "/dev/xvdd",
   432  					},
   433  				},
   434  			},
   435  			expectedType: &dockerConfig{},
   436  		},
   437  	}
   438  
   439  	for _, c := range cases {
   440  		t.Run(c.name, func(t *testing.T) {
   441  			t.Logf("Val: % #v", pretty.Formatter(c.config))
   442  			// Parse the interface
   443  			ctyValue, diag := ParseHclInterface(c.config, c.spec, c.ctx)
   444  			if diag.HasErrors() {
   445  				for _, err := range diag.Errs() {
   446  					t.Error(err)
   447  				}
   448  				t.FailNow()
   449  			}
   450  
   451  			// Convert cty-value to go structs
   452  			require.NoError(t, gocty.FromCtyValue(ctyValue, c.expectedType))
   453  
   454  			require.EqualValues(t, c.expected, c.expectedType)
   455  
   456  		})
   457  	}
   458  }