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 }