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  `