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 }