github.com/hashicorp/hcl/v2@v2.20.0/integrationtest/terraformlike_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package integrationtest 5 6 import ( 7 "reflect" 8 "sort" 9 "testing" 10 11 "github.com/davecgh/go-spew/spew" 12 "github.com/hashicorp/hcl/v2" 13 "github.com/hashicorp/hcl/v2/ext/dynblock" 14 "github.com/hashicorp/hcl/v2/gohcl" 15 "github.com/hashicorp/hcl/v2/hcldec" 16 "github.com/hashicorp/hcl/v2/hclsyntax" 17 "github.com/hashicorp/hcl/v2/json" 18 "github.com/zclconf/go-cty/cty" 19 "github.com/zclconf/go-cty/cty/function" 20 ) 21 22 // TestTerraformLike parses both a native syntax and a JSON representation 23 // of the same HashiCorp Terraform-like configuration structure and then makes 24 // assertions against the result of each. 25 // 26 // Terraform exercises a lot of different HCL codepaths, so this is not 27 // exhaustive but tries to cover a variety of different relevant scenarios. 28 func TestTerraformLike(t *testing.T) { 29 tests := map[string]func() (*hcl.File, hcl.Diagnostics){ 30 "native syntax": func() (*hcl.File, hcl.Diagnostics) { 31 return hclsyntax.ParseConfig( 32 []byte(terraformLikeNativeSyntax), 33 "config.tf", hcl.Pos{Line: 1, Column: 1}, 34 ) 35 }, 36 "JSON": func() (*hcl.File, hcl.Diagnostics) { 37 return json.Parse( 38 []byte(terraformLikeJSON), 39 "config.tf.json", 40 ) 41 }, 42 } 43 44 type Variable struct { 45 Name string `hcl:"name,label"` 46 } 47 type Resource struct { 48 Type string `hcl:"type,label"` 49 Name string `hcl:"name,label"` 50 Config hcl.Body `hcl:",remain"` 51 DependsOn hcl.Expression `hcl:"depends_on,attr"` 52 } 53 type Module struct { 54 Name string `hcl:"name,label"` 55 Providers hcl.Expression `hcl:"providers"` 56 } 57 type Locals struct { 58 Config hcl.Body `hcl:",remain"` 59 } 60 type Root struct { 61 Variables []*Variable `hcl:"variable,block"` 62 Resources []*Resource `hcl:"resource,block"` 63 Modules []*Module `hcl:"module,block"` 64 Locals []*Locals `hcl:"locals,block"` 65 } 66 instanceDecode := &hcldec.ObjectSpec{ 67 "image_id": &hcldec.AttrSpec{ 68 Name: "image_id", 69 Required: true, 70 Type: cty.String, 71 }, 72 "instance_type": &hcldec.AttrSpec{ 73 Name: "instance_type", 74 Required: true, 75 Type: cty.String, 76 }, 77 "tags": &hcldec.AttrSpec{ 78 Name: "tags", 79 Required: false, 80 Type: cty.Map(cty.String), 81 }, 82 } 83 securityGroupDecode := &hcldec.ObjectSpec{ 84 "ingress": &hcldec.BlockListSpec{ 85 TypeName: "ingress", 86 Nested: &hcldec.ObjectSpec{ 87 "cidr_block": &hcldec.AttrSpec{ 88 Name: "cidr_block", 89 Required: true, 90 Type: cty.String, 91 }, 92 }, 93 }, 94 } 95 96 for name, loadFunc := range tests { 97 t.Run(name, func(t *testing.T) { 98 file, diags := loadFunc() 99 if len(diags) != 0 { 100 t.Errorf("unexpected diagnostics during parse") 101 for _, diag := range diags { 102 t.Logf("- %s", diag) 103 } 104 return 105 } 106 107 body := file.Body 108 109 var root Root 110 diags = gohcl.DecodeBody(body, nil, &root) 111 if len(diags) != 0 { 112 t.Errorf("unexpected diagnostics during root eval") 113 for _, diag := range diags { 114 t.Logf("- %s", diag) 115 } 116 return 117 } 118 119 wantVars := []*Variable{ 120 { 121 Name: "image_id", 122 }, 123 } 124 if gotVars := root.Variables; !reflect.DeepEqual(gotVars, wantVars) { 125 t.Errorf("wrong Variables\ngot: %swant: %s", spew.Sdump(gotVars), spew.Sdump(wantVars)) 126 } 127 128 if got, want := len(root.Resources), 3; got != want { 129 t.Fatalf("wrong number of Resources %d; want %d", got, want) 130 } 131 132 sort.Slice(root.Resources, func(i, j int) bool { 133 return root.Resources[i].Name < root.Resources[j].Name 134 }) 135 136 t.Run("resource 0", func(t *testing.T) { 137 r := root.Resources[0] 138 if got, want := r.Type, "happycloud_security_group"; got != want { 139 t.Errorf("wrong type %q; want %q", got, want) 140 } 141 if got, want := r.Name, "private"; got != want { 142 t.Errorf("wrong type %q; want %q", got, want) 143 } 144 145 // For this one we're including support for the dynamic block 146 // extension, since Terraform uses this to allow dynamic 147 // generation of blocks within resource configuration. 148 forEachCtx := &hcl.EvalContext{ 149 Variables: map[string]cty.Value{ 150 "var": cty.ObjectVal(map[string]cty.Value{ 151 "extra_private_cidr_blocks": cty.ListVal([]cty.Value{ 152 cty.StringVal("172.16.0.0/12"), 153 cty.StringVal("169.254.0.0/16"), 154 }), 155 }), 156 }, 157 } 158 dynBody := dynblock.Expand(r.Config, forEachCtx) 159 160 cfg, diags := hcldec.Decode(dynBody, securityGroupDecode, nil) 161 if len(diags) != 0 { 162 t.Errorf("unexpected diagnostics decoding Config") 163 for _, diag := range diags { 164 t.Logf("- %s", diag) 165 } 166 return 167 } 168 wantCfg := cty.ObjectVal(map[string]cty.Value{ 169 "ingress": cty.ListVal([]cty.Value{ 170 cty.ObjectVal(map[string]cty.Value{ 171 "cidr_block": cty.StringVal("10.0.0.0/8"), 172 }), 173 cty.ObjectVal(map[string]cty.Value{ 174 "cidr_block": cty.StringVal("192.168.0.0/16"), 175 }), 176 cty.ObjectVal(map[string]cty.Value{ 177 "cidr_block": cty.StringVal("172.16.0.0/12"), 178 }), 179 cty.ObjectVal(map[string]cty.Value{ 180 "cidr_block": cty.StringVal("169.254.0.0/16"), 181 }), 182 }), 183 }) 184 if !cfg.RawEquals(wantCfg) { 185 t.Errorf("wrong config\ngot: %#v\nwant: %#v", cfg, wantCfg) 186 } 187 }) 188 189 t.Run("resource 1", func(t *testing.T) { 190 r := root.Resources[1] 191 if got, want := r.Type, "happycloud_security_group"; got != want { 192 t.Errorf("wrong type %q; want %q", got, want) 193 } 194 if got, want := r.Name, "public"; got != want { 195 t.Errorf("wrong type %q; want %q", got, want) 196 } 197 198 cfg, diags := hcldec.Decode(r.Config, securityGroupDecode, nil) 199 if len(diags) != 0 { 200 t.Errorf("unexpected diagnostics decoding Config") 201 for _, diag := range diags { 202 t.Logf("- %s", diag) 203 } 204 return 205 } 206 wantCfg := cty.ObjectVal(map[string]cty.Value{ 207 "ingress": cty.ListVal([]cty.Value{ 208 cty.ObjectVal(map[string]cty.Value{ 209 "cidr_block": cty.StringVal("0.0.0.0/0"), 210 }), 211 }), 212 }) 213 if !cfg.RawEquals(wantCfg) { 214 t.Errorf("wrong config\ngot: %#v\nwant: %#v", cfg, wantCfg) 215 } 216 }) 217 218 t.Run("resource 2", func(t *testing.T) { 219 r := root.Resources[2] 220 if got, want := r.Type, "happycloud_instance"; got != want { 221 t.Errorf("wrong type %q; want %q", got, want) 222 } 223 if got, want := r.Name, "test"; got != want { 224 t.Errorf("wrong type %q; want %q", got, want) 225 } 226 227 vars := hcldec.Variables(r.Config, &hcldec.AttrSpec{ 228 Name: "image_id", 229 Type: cty.String, 230 }) 231 if got, want := len(vars), 1; got != want { 232 t.Errorf("wrong number of variables in image_id %#v; want %#v", got, want) 233 } 234 if got, want := vars[0].RootName(), "var"; got != want { 235 t.Errorf("wrong image_id variable RootName %#v; want %#v", got, want) 236 } 237 238 ctx := &hcl.EvalContext{ 239 Variables: map[string]cty.Value{ 240 "var": cty.ObjectVal(map[string]cty.Value{ 241 "image_id": cty.StringVal("image-1234"), 242 }), 243 }, 244 } 245 cfg, diags := hcldec.Decode(r.Config, instanceDecode, ctx) 246 if len(diags) != 0 { 247 t.Errorf("unexpected diagnostics decoding Config") 248 for _, diag := range diags { 249 t.Logf("- %s", diag) 250 } 251 return 252 } 253 wantCfg := cty.ObjectVal(map[string]cty.Value{ 254 "instance_type": cty.StringVal("z3.weedy"), 255 "image_id": cty.StringVal("image-1234"), 256 "tags": cty.MapVal(map[string]cty.Value{ 257 "Name": cty.StringVal("foo"), 258 "Environment": cty.StringVal("prod"), 259 }), 260 }) 261 if !cfg.RawEquals(wantCfg) { 262 t.Errorf("wrong config\ngot: %#v\nwant: %#v", cfg, wantCfg) 263 } 264 265 exprs, diags := hcl.ExprList(r.DependsOn) 266 if len(diags) != 0 { 267 t.Errorf("unexpected diagnostics extracting depends_on") 268 for _, diag := range diags { 269 t.Logf("- %s", diag) 270 } 271 return 272 } 273 if got, want := len(exprs), 1; got != want { 274 t.Errorf("wrong number of depends_on exprs %#v; want %#v", got, want) 275 } 276 277 traversal, diags := hcl.AbsTraversalForExpr(exprs[0]) 278 if len(diags) != 0 { 279 t.Errorf("unexpected diagnostics decoding depends_on[0]") 280 for _, diag := range diags { 281 t.Logf("- %s", diag) 282 } 283 return 284 } 285 if got, want := len(traversal), 2; got != want { 286 t.Errorf("wrong number of depends_on traversal steps %#v; want %#v", got, want) 287 } 288 if got, want := traversal.RootName(), "happycloud_security_group"; got != want { 289 t.Errorf("wrong depends_on traversal RootName %#v; want %#v", got, want) 290 } 291 }) 292 293 t.Run("module", func(t *testing.T) { 294 if got, want := len(root.Modules), 1; got != want { 295 t.Fatalf("wrong number of Modules %d; want %d", got, want) 296 } 297 mod := root.Modules[0] 298 if got, want := mod.Name, "foo"; got != want { 299 t.Errorf("wrong module name %q; want %q", got, want) 300 } 301 302 pExpr := mod.Providers 303 pairs, diags := hcl.ExprMap(pExpr) 304 if len(diags) != 0 { 305 t.Errorf("unexpected diagnostics extracting providers") 306 for _, diag := range diags { 307 t.Logf("- %s", diag) 308 } 309 } 310 if got, want := len(pairs), 1; got != want { 311 t.Fatalf("wrong number of key/value pairs in providers %d; want %d", got, want) 312 } 313 314 pair := pairs[0] 315 kt, diags := hcl.AbsTraversalForExpr(pair.Key) 316 if len(diags) != 0 { 317 t.Errorf("unexpected diagnostics extracting providers key %#v", pair.Key) 318 for _, diag := range diags { 319 t.Logf("- %s", diag) 320 } 321 } 322 vt, diags := hcl.AbsTraversalForExpr(pair.Value) 323 if len(diags) != 0 { 324 t.Errorf("unexpected diagnostics extracting providers value %#v", pair.Value) 325 for _, diag := range diags { 326 t.Logf("- %s", diag) 327 } 328 } 329 330 if got, want := len(kt), 1; got != want { 331 t.Fatalf("wrong number of key traversal steps %d; want %d", got, want) 332 } 333 if got, want := len(vt), 2; got != want { 334 t.Fatalf("wrong number of value traversal steps %d; want %d", got, want) 335 } 336 337 if got, want := kt.RootName(), "null"; got != want { 338 t.Errorf("wrong number key traversal root %s; want %s", got, want) 339 } 340 if got, want := vt.RootName(), "null"; got != want { 341 t.Errorf("wrong number value traversal root %s; want %s", got, want) 342 } 343 if at, ok := vt[1].(hcl.TraverseAttr); ok { 344 if got, want := at.Name, "foo"; got != want { 345 t.Errorf("wrong number value traversal attribute name %s; want %s", got, want) 346 } 347 } else { 348 t.Errorf("wrong value traversal [1] type %T; want hcl.TraverseAttr", vt[1]) 349 } 350 }) 351 352 t.Run("locals", func(t *testing.T) { 353 locals := root.Locals[0] 354 attrs, diags := locals.Config.JustAttributes() 355 if diags.HasErrors() { 356 t.Fatal(diags) 357 } 358 359 ctx := &hcl.EvalContext{ 360 Functions: map[string]function.Function{ 361 "func": function.New(&function.Spec{ 362 Params: []function.Parameter{{Type: cty.String}}, 363 Type: function.StaticReturnType(cty.String), 364 Impl: func([]cty.Value, cty.Type) (cty.Value, error) { 365 return cty.StringVal("func_result"), nil 366 }, 367 }), 368 "scoped::func": function.New(&function.Spec{ 369 Params: []function.Parameter{{Type: cty.String}}, 370 Type: function.StaticReturnType(cty.String), 371 Impl: func([]cty.Value, cty.Type) (cty.Value, error) { 372 return cty.StringVal("scoped::func_result"), nil 373 }, 374 }), 375 }, 376 } 377 378 res := attrs["func_result"] 379 funcVal, diags := res.Expr.Value(ctx) 380 if diags.HasErrors() { 381 t.Fatal(diags) 382 } 383 384 wantVal := cty.StringVal("func_result") 385 386 if !funcVal.RawEquals(wantVal) { 387 t.Errorf("expected %#v, got %#v", wantVal, funcVal) 388 } 389 390 res = attrs["scoped_func_result"] 391 funcVal, diags = res.Expr.Value(ctx) 392 if diags.HasErrors() { 393 t.Fatal(diags) 394 } 395 396 wantVal = cty.StringVal("scoped::func_result") 397 398 if !funcVal.RawEquals(wantVal) { 399 t.Errorf("expected %#v, got %#v", wantVal, funcVal) 400 } 401 }) 402 }) 403 } 404 } 405 406 const terraformLikeNativeSyntax = ` 407 408 variable "image_id" { 409 } 410 411 locals { 412 func_result = func("arg") 413 scoped_func_result = scoped::func("arg") 414 } 415 416 resource "happycloud_instance" "test" { 417 instance_type = "z3.weedy" 418 image_id = var.image_id 419 420 tags = { 421 "Name" = "foo" 422 "${"Environment"}" = "prod" 423 } 424 425 depends_on = [ 426 happycloud_security_group.public, 427 ] 428 } 429 430 resource "happycloud_security_group" "public" { 431 ingress { 432 cidr_block = "0.0.0.0/0" 433 } 434 } 435 436 resource "happycloud_security_group" "private" { 437 ingress { 438 cidr_block = "10.0.0.0/8" 439 } 440 ingress { 441 cidr_block = "192.168.0.0/16" 442 } 443 dynamic "ingress" { 444 for_each = var.extra_private_cidr_blocks 445 content { 446 cidr_block = ingress.value 447 } 448 } 449 } 450 451 module "foo" { 452 providers = { 453 null = null.foo 454 } 455 } 456 457 ` 458 459 const terraformLikeJSON = ` 460 { 461 "variable": { 462 "image_id": {} 463 }, 464 "locals": { 465 "func_result": "${func(\"arg\")}", 466 "scoped_func_result": "${scoped::func(\"arg\")}" 467 }, 468 "resource": { 469 "happycloud_instance": { 470 "test": { 471 "instance_type": "z3.weedy", 472 "image_id": "${var.image_id}", 473 "tags": { 474 "Name": "foo", 475 "${\"Environment\"}": "prod" 476 }, 477 "depends_on": [ 478 "happycloud_security_group.public" 479 ] 480 } 481 }, 482 "happycloud_security_group": { 483 "public": { 484 "ingress": { 485 "cidr_block": "0.0.0.0/0" 486 } 487 }, 488 "private": { 489 "ingress": [ 490 { 491 "cidr_block": "10.0.0.0/8" 492 }, 493 { 494 "cidr_block": "192.168.0.0/16" 495 } 496 ], 497 "dynamic": { 498 "ingress": { 499 "for_each": "${var.extra_private_cidr_blocks}", 500 "iterator": "block", 501 "content": { 502 "cidr_block": "${block.value}" 503 } 504 } 505 } 506 } 507 } 508 }, 509 "module": { 510 "foo": { 511 "providers": { 512 "null": "null.foo" 513 } 514 } 515 } 516 } 517 `