hcl/hcl/integrationtest/terraformlike_test.go
Martin Atkins eea3a14a71 hcl/json: allow more flexible use of arrays when describing bodies
Previously we allowed arrays only at the "leaf" of a set of objects
describing a block and its labels. This is not sufficient because it is
therefore impossible to preserve the relative ordering of a sequence
of blocks that have different block types or labels.

The spec now allows arrays of objects to be used in place of single
objects when that value is representing either an HCL body or a set of
labels on a nested block. This relaxing does not apply to JSON objects
interpreted as expressions or bodies interpreted in dynamic attributes
mode, since there is no requirement to preserve attribute ordering or
support duplicate property names in those scenarios.

This new model imposes additional constraints on the underlying JSON
parser used to interpret JSON HCL: it must now be able to retain the
relative ordering of object keys and accept multiple definitions of the
same key. This requirement is not imposed on _producers_, which are free
to use the allowance for arrays of objects to force ordering and duplicate
keys with JSON-producing libraries that are unable to make these
distinctions.

Since we are now requiring a specialized parser anyway, we also require
that it be able to represent numbers at full precision, whereas before
we made some allowances for implementations to not support this.
2018-02-17 10:26:58 -08:00

373 lines
9.6 KiB
Go

package integrationtest
import (
"reflect"
"sort"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/hcl2/ext/dynblock"
"github.com/hashicorp/hcl2/gohcl"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/hashicorp/hcl2/hcl/json"
"github.com/hashicorp/hcl2/hcldec"
"github.com/zclconf/go-cty/cty"
)
// TestTerraformLike parses both a native syntax and a JSON representation
// of the same HashiCorp Terraform-like configuration structure and then makes
// assertions against the result of each.
//
// Terraform exercises a lot of different HCL codepaths, so this is not
// exhaustive but tries to cover a variety of different relevant scenarios.
func TestTerraformLike(t *testing.T) {
tests := map[string]func() (*hcl.File, hcl.Diagnostics){
"native syntax": func() (*hcl.File, hcl.Diagnostics) {
return hclsyntax.ParseConfig(
[]byte(terraformLikeNativeSyntax),
"config.tf", hcl.Pos{Line: 1, Column: 1},
)
},
"JSON": func() (*hcl.File, hcl.Diagnostics) {
return json.Parse(
[]byte(terraformLikeJSON),
"config.tf.json",
)
},
}
type Variable struct {
Name string `hcl:"name,label"`
}
type Resource struct {
Type string `hcl:"type,label"`
Name string `hcl:"name,label"`
Config hcl.Body `hcl:",remain"`
DependsOn hcl.Expression `hcl:"depends_on,attr"`
}
type Root struct {
Variables []*Variable `hcl:"variable,block"`
Resources []*Resource `hcl:"resource,block"`
}
instanceDecode := &hcldec.ObjectSpec{
"image_id": &hcldec.AttrSpec{
Name: "image_id",
Required: true,
Type: cty.String,
},
"instance_type": &hcldec.AttrSpec{
Name: "instance_type",
Required: true,
Type: cty.String,
},
"tags": &hcldec.AttrSpec{
Name: "tags",
Required: false,
Type: cty.Map(cty.String),
},
}
securityGroupDecode := &hcldec.ObjectSpec{
"ingress": &hcldec.BlockListSpec{
TypeName: "ingress",
Nested: &hcldec.ObjectSpec{
"cidr_block": &hcldec.AttrSpec{
Name: "cidr_block",
Required: true,
Type: cty.String,
},
},
},
}
for name, loadFunc := range tests {
t.Run(name, func(t *testing.T) {
file, diags := loadFunc()
if len(diags) != 0 {
t.Errorf("unexpected diagnostics during parse")
for _, diag := range diags {
t.Logf("- %s", diag)
}
return
}
body := file.Body
var root Root
diags = gohcl.DecodeBody(body, nil, &root)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics during root eval")
for _, diag := range diags {
t.Logf("- %s", diag)
}
return
}
wantVars := []*Variable{
{
Name: "image_id",
},
}
if gotVars := root.Variables; !reflect.DeepEqual(gotVars, wantVars) {
t.Errorf("wrong Variables\ngot: %swant: %s", spew.Sdump(gotVars), spew.Sdump(wantVars))
}
if got, want := len(root.Resources), 3; got != want {
t.Fatalf("wrong number of Resources %d; want %d", got, want)
}
sort.Slice(root.Resources, func(i, j int) bool {
return root.Resources[i].Name < root.Resources[j].Name
})
t.Run("resource 0", func(t *testing.T) {
r := root.Resources[0]
if got, want := r.Type, "happycloud_security_group"; got != want {
t.Errorf("wrong type %q; want %q", got, want)
}
if got, want := r.Name, "private"; got != want {
t.Errorf("wrong type %q; want %q", got, want)
}
// For this one we're including support for the dynamic block
// extension, since Terraform uses this to allow dynamic
// generation of blocks within resource configuration.
forEachCtx := &hcl.EvalContext{
Variables: map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
"extra_private_cidr_blocks": cty.ListVal([]cty.Value{
cty.StringVal("172.16.0.0/12"),
cty.StringVal("169.254.0.0/16"),
}),
}),
},
}
dynBody := dynblock.Expand(r.Config, forEachCtx)
cfg, diags := hcldec.Decode(dynBody, securityGroupDecode, nil)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics decoding Config")
for _, diag := range diags {
t.Logf("- %s", diag)
}
return
}
wantCfg := cty.ObjectVal(map[string]cty.Value{
"ingress": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"cidr_block": cty.StringVal("10.0.0.0/8"),
}),
cty.ObjectVal(map[string]cty.Value{
"cidr_block": cty.StringVal("192.168.0.0/16"),
}),
cty.ObjectVal(map[string]cty.Value{
"cidr_block": cty.StringVal("172.16.0.0/12"),
}),
cty.ObjectVal(map[string]cty.Value{
"cidr_block": cty.StringVal("169.254.0.0/16"),
}),
}),
})
if !cfg.RawEquals(wantCfg) {
t.Errorf("wrong config\ngot: %#v\nwant: %#v", cfg, wantCfg)
}
})
t.Run("resource 1", func(t *testing.T) {
r := root.Resources[1]
if got, want := r.Type, "happycloud_security_group"; got != want {
t.Errorf("wrong type %q; want %q", got, want)
}
if got, want := r.Name, "public"; got != want {
t.Errorf("wrong type %q; want %q", got, want)
}
cfg, diags := hcldec.Decode(r.Config, securityGroupDecode, nil)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics decoding Config")
for _, diag := range diags {
t.Logf("- %s", diag)
}
return
}
wantCfg := cty.ObjectVal(map[string]cty.Value{
"ingress": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"cidr_block": cty.StringVal("0.0.0.0/0"),
}),
}),
})
if !cfg.RawEquals(wantCfg) {
t.Errorf("wrong config\ngot: %#v\nwant: %#v", cfg, wantCfg)
}
})
t.Run("resource 2", func(t *testing.T) {
r := root.Resources[2]
if got, want := r.Type, "happycloud_instance"; got != want {
t.Errorf("wrong type %q; want %q", got, want)
}
if got, want := r.Name, "test"; got != want {
t.Errorf("wrong type %q; want %q", got, want)
}
vars := hcldec.Variables(r.Config, &hcldec.AttrSpec{
Name: "image_id",
Type: cty.String,
})
if got, want := len(vars), 1; got != want {
t.Errorf("wrong number of variables in image_id %#v; want %#v", got, want)
}
if got, want := vars[0].RootName(), "var"; got != want {
t.Errorf("wrong image_id variable RootName %#v; want %#v", got, want)
}
ctx := &hcl.EvalContext{
Variables: map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
"image_id": cty.StringVal("image-1234"),
}),
},
}
cfg, diags := hcldec.Decode(r.Config, instanceDecode, ctx)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics decoding Config")
for _, diag := range diags {
t.Logf("- %s", diag)
}
return
}
wantCfg := cty.ObjectVal(map[string]cty.Value{
"instance_type": cty.StringVal("z3.weedy"),
"image_id": cty.StringVal("image-1234"),
"tags": cty.MapVal(map[string]cty.Value{
"Name": cty.StringVal("foo"),
"Environment": cty.StringVal("prod"),
}),
})
if !cfg.RawEquals(wantCfg) {
t.Errorf("wrong config\ngot: %#v\nwant: %#v", cfg, wantCfg)
}
exprs, diags := hcl.ExprList(r.DependsOn)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics extracting depends_on")
for _, diag := range diags {
t.Logf("- %s", diag)
}
return
}
if got, want := len(exprs), 1; got != want {
t.Errorf("wrong number of depends_on exprs %#v; want %#v", got, want)
}
traversal, diags := hcl.AbsTraversalForExpr(exprs[0])
if len(diags) != 0 {
t.Errorf("unexpected diagnostics decoding depends_on[0]")
for _, diag := range diags {
t.Logf("- %s", diag)
}
return
}
if got, want := len(traversal), 2; got != want {
t.Errorf("wrong number of depends_on traversal steps %#v; want %#v", got, want)
}
if got, want := traversal.RootName(), "happycloud_security_group"; got != want {
t.Errorf("wrong depends_on traversal RootName %#v; want %#v", got, want)
}
})
})
}
}
const terraformLikeNativeSyntax = `
variable "image_id" {
}
resource "happycloud_instance" "test" {
instance_type = "z3.weedy"
image_id = var.image_id
tags = {
"Name" = "foo"
"${"Environment"}" = "prod"
}
depends_on = [
happycloud_security_group.public,
]
}
resource "happycloud_security_group" "public" {
ingress {
cidr_block = "0.0.0.0/0"
}
}
resource "happycloud_security_group" "private" {
ingress {
cidr_block = "10.0.0.0/8"
}
ingress {
cidr_block = "192.168.0.0/16"
}
dynamic "ingress" {
for_each = var.extra_private_cidr_blocks
content {
cidr_block = ingress.value
}
}
}
`
const terraformLikeJSON = `
{
"variable": {
"image_id": {}
},
"resource": {
"happycloud_instance": {
"test": {
"instance_type": "z3.weedy",
"image_id": "${var.image_id}",
"tags": {
"Name": "foo",
"${\"Environment\"}": "prod"
},
"depends_on": [
"happycloud_security_group.public"
]
}
},
"happycloud_security_group": {
"public": {
"ingress": {
"cidr_block": "0.0.0.0/0"
}
},
"private": {
"ingress": [
{
"cidr_block": "10.0.0.0/8"
},
{
"cidr_block": "192.168.0.0/16"
}
],
"dynamic": {
"ingress": {
"for_each": "${var.extra_private_cidr_blocks}",
"iterator": "block",
"content": {
"cidr_block": "${block.value}"
}
}
}
}
}
}
}
`