opentofu/config/loader_hcl2_test.go
Martin Atkins b0215fcd0f config: HCL2 config loader
This loader uses the HCL2 parser and decoder to process a config file,
and then transforms the result into the same shape as would be produced
by the HCL config loader.

To avoid making changes to the existing config structures (which are
depended on across much of the codebase) we first decode into a set of
HCL2-tailored structs and then process them into the public-facing structs
that a loader is expected to return. This is a compromise to keep the
config package API broadly unchanged for now. Once we're ready to remove
the old HCL loader (which implies that we're ready to support HCL2
natively elsewhere in the codebase) we will be able to simplify this
quite considerably.

Due to some mismatches of abstraction between HCL/HIL and HCL2, some
shimming is required to get the required result.
2017-10-03 17:47:01 -07:00

511 lines
16 KiB
Go

package config
import (
"reflect"
"testing"
"github.com/zclconf/go-cty/cty"
gohcl2 "github.com/hashicorp/hcl2/gohcl"
hcl2 "github.com/hashicorp/hcl2/hcl"
)
func TestHCL2ConfigurableConfigurable(t *testing.T) {
var _ configurable = new(hcl2Configurable)
}
func TestHCL2Basic(t *testing.T) {
loader := globalHCL2Loader
cbl, _, err := loader.loadFile("test-fixtures/basic-hcl2.tf")
if err != nil {
if diags, isDiags := err.(hcl2.Diagnostics); isDiags {
for _, diag := range diags {
t.Logf("- %s", diag.Error())
}
t.Fatalf("unexpected diagnostics in load")
} else {
t.Fatalf("unexpected error in load: %s", err)
}
}
cfg, err := cbl.Config()
if err != nil {
if diags, isDiags := err.(hcl2.Diagnostics); isDiags {
for _, diag := range diags {
t.Logf("- %s", diag.Error())
}
t.Fatalf("unexpected diagnostics in decode")
} else {
t.Fatalf("unexpected error in decode: %s", err)
}
}
// Unfortunately the config structure isn't DeepEqual-friendly because
// of all the nested RawConfig, etc structures, so we'll need to
// hand-assert each item.
// The "terraform" block
if cfg.Terraform == nil {
t.Fatalf("Terraform field is nil")
}
if got, want := cfg.Terraform.RequiredVersion, "foo"; got != want {
t.Errorf("wrong Terraform.RequiredVersion %q; want %q", got, want)
}
if cfg.Terraform.Backend == nil {
t.Fatalf("Terraform.Backend is nil")
}
if got, want := cfg.Terraform.Backend.Type, "baz"; got != want {
t.Errorf("wrong Terraform.Backend.Type %q; want %q", got, want)
}
if got, want := cfg.Terraform.Backend.RawConfig.Raw, map[string]interface{}{"something": "nothing"}; !reflect.DeepEqual(got, want) {
t.Errorf("wrong Terraform.Backend.RawConfig.Raw %#v; want %#v", got, want)
}
// The "atlas" block
if cfg.Atlas == nil {
t.Fatalf("Atlas field is nil")
}
if got, want := cfg.Atlas.Name, "example/foo"; got != want {
t.Errorf("wrong Atlas.Name %q; want %q", got, want)
}
// "module" blocks
if got, want := len(cfg.Modules), 1; got != want {
t.Errorf("Modules slice has wrong length %#v; want %#v", got, want)
} else {
m := cfg.Modules[0]
if got, want := m.Name, "child"; got != want {
t.Errorf("wrong Modules[0].Name %#v; want %#v", got, want)
}
if got, want := m.Source, "./baz"; got != want {
t.Errorf("wrong Modules[0].Source %#v; want %#v", got, want)
}
want := map[string]string{"toasty": "true"}
var got map[string]string
gohcl2.DecodeBody(m.RawConfig.Body, nil, &got)
if !reflect.DeepEqual(got, want) {
t.Errorf("wrong Modules[0].RawConfig.Body %#v; want %#v", got, want)
}
}
// "resource" blocks
if got, want := len(cfg.Resources), 5; got != want {
t.Errorf("Resources slice has wrong length %#v; want %#v", got, want)
} else {
{
r := cfg.Resources[0]
if got, want := r.Id(), "aws_security_group.firewall"; got != want {
t.Errorf("wrong Resources[0].Id() %#v; want %#v", got, want)
}
wantConfig := map[string]string{}
var gotConfig map[string]string
gohcl2.DecodeBody(r.RawConfig.Body, nil, &gotConfig)
if !reflect.DeepEqual(gotConfig, wantConfig) {
t.Errorf("wrong Resources[0].RawConfig.Body %#v; want %#v", gotConfig, wantConfig)
}
wantCount := map[string]string{"count": "5"}
var gotCount map[string]string
gohcl2.DecodeBody(r.RawCount.Body, nil, &gotCount)
if !reflect.DeepEqual(gotCount, wantCount) {
t.Errorf("wrong Resources[0].RawCount.Body %#v; want %#v", gotCount, wantCount)
}
if got, want := r.RawCount.Key, "count"; got != want {
t.Errorf("wrong Resources[0].RawCount.Key %#v; want %#v", got, want)
}
if got, want := len(r.Provisioners), 0; got != want {
t.Errorf("wrong Resources[0].Provisioners length %#v; want %#v", got, want)
}
if got, want := len(r.DependsOn), 0; got != want {
t.Errorf("wrong Resources[0].DependsOn length %#v; want %#v", got, want)
}
if got, want := r.Provider, "another"; got != want {
t.Errorf("wrong Resources[0].Provider %#v; want %#v", got, want)
}
if got, want := r.Lifecycle, (ResourceLifecycle{}); !reflect.DeepEqual(got, want) {
t.Errorf("wrong Resources[0].Lifecycle %#v; want %#v", got, want)
}
}
{
r := cfg.Resources[1]
if got, want := r.Id(), "aws_instance.web"; got != want {
t.Errorf("wrong Resources[1].Id() %#v; want %#v", got, want)
}
if got, want := r.Provider, ""; got != want {
t.Errorf("wrong Resources[1].Provider %#v; want %#v", got, want)
}
if got, want := len(r.Provisioners), 1; got != want {
t.Errorf("wrong Resources[1].Provisioners length %#v; want %#v", got, want)
} else {
p := r.Provisioners[0]
if got, want := p.Type, "file"; got != want {
t.Errorf("wrong Resources[1].Provisioners[0].Type %#v; want %#v", got, want)
}
wantConfig := map[string]string{
"source": "foo",
"destination": "bar",
}
var gotConfig map[string]string
gohcl2.DecodeBody(p.RawConfig.Body, nil, &gotConfig)
if !reflect.DeepEqual(gotConfig, wantConfig) {
t.Errorf("wrong Resources[1].Provisioners[0].RawConfig.Body %#v; want %#v", gotConfig, wantConfig)
}
wantConn := map[string]string{
"default": "true",
}
var gotConn map[string]string
gohcl2.DecodeBody(p.ConnInfo.Body, nil, &gotConn)
if !reflect.DeepEqual(gotConn, wantConn) {
t.Errorf("wrong Resources[1].Provisioners[0].ConnInfo.Body %#v; want %#v", gotConn, wantConn)
}
}
// We'll use these throwaway structs to more easily decode and
// compare the main config body.
type instanceNetworkInterface struct {
DeviceIndex int `hcl:"device_index"`
Description string `hcl:"description"`
}
type instanceConfig struct {
AMI string `hcl:"ami"`
SecurityGroups []string `hcl:"security_groups"`
NetworkInterface instanceNetworkInterface `hcl:"network_interface,block"`
}
var gotConfig instanceConfig
wantConfig := instanceConfig{
AMI: "ami-abc123",
SecurityGroups: []string{"foo", "sg-firewall"},
NetworkInterface: instanceNetworkInterface{
DeviceIndex: 0,
Description: "Main network interface",
},
}
ctx := &hcl2.EvalContext{
Variables: map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("ami-abc123"),
}),
"aws_security_group": cty.ObjectVal(map[string]cty.Value{
"firewall": cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("sg-firewall"),
}),
}),
},
}
diags := gohcl2.DecodeBody(r.RawConfig.Body, ctx, &gotConfig)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics decoding Resources[1].RawConfig.Body")
for _, diag := range diags {
t.Logf("- %s", diag.Error())
}
}
if !reflect.DeepEqual(gotConfig, wantConfig) {
t.Errorf("wrong Resources[1].RawConfig.Body %#v; want %#v", gotConfig, wantConfig)
}
}
{
r := cfg.Resources[2]
if got, want := r.Id(), "aws_instance.db"; got != want {
t.Errorf("wrong Resources[2].Id() %#v; want %#v", got, want)
}
if got, want := r.DependsOn, []string{"aws_instance.web"}; !reflect.DeepEqual(got, want) {
t.Errorf("wrong Resources[2].DependsOn %#v; want %#v", got, want)
}
if got, want := len(r.Provisioners), 1; got != want {
t.Errorf("wrong Resources[2].Provisioners length %#v; want %#v", got, want)
} else {
p := r.Provisioners[0]
if got, want := p.Type, "file"; got != want {
t.Errorf("wrong Resources[2].Provisioners[0].Type %#v; want %#v", got, want)
}
wantConfig := map[string]string{
"source": "here",
"destination": "there",
}
var gotConfig map[string]string
gohcl2.DecodeBody(p.RawConfig.Body, nil, &gotConfig)
if !reflect.DeepEqual(gotConfig, wantConfig) {
t.Errorf("wrong Resources[2].Provisioners[0].RawConfig.Body %#v; want %#v", gotConfig, wantConfig)
}
wantConn := map[string]string{
"default": "false",
}
var gotConn map[string]string
gohcl2.DecodeBody(p.ConnInfo.Body, nil, &gotConn)
if !reflect.DeepEqual(gotConn, wantConn) {
t.Errorf("wrong Resources[2].Provisioners[0].ConnInfo.Body %#v; want %#v", gotConn, wantConn)
}
}
}
{
r := cfg.Resources[3]
if got, want := r.Id(), "data.do.simple"; got != want {
t.Errorf("wrong Resources[3].Id() %#v; want %#v", got, want)
}
if got, want := r.DependsOn, []string(nil); !reflect.DeepEqual(got, want) {
t.Errorf("wrong Resources[3].DependsOn %#v; want %#v", got, want)
}
if got, want := r.Provider, "do.foo"; got != want {
t.Errorf("wrong Resources[3].Provider %#v; want %#v", got, want)
}
wantConfig := map[string]string{
"foo": "baz",
}
var gotConfig map[string]string
gohcl2.DecodeBody(r.RawConfig.Body, nil, &gotConfig)
if !reflect.DeepEqual(gotConfig, wantConfig) {
t.Errorf("wrong Resources[3].RawConfig.Body %#v; want %#v", gotConfig, wantConfig)
}
}
{
r := cfg.Resources[4]
if got, want := r.Id(), "data.do.depends"; got != want {
t.Errorf("wrong Resources[4].Id() %#v; want %#v", got, want)
}
if got, want := r.DependsOn, []string{"data.do.simple"}; !reflect.DeepEqual(got, want) {
t.Errorf("wrong Resources[4].DependsOn %#v; want %#v", got, want)
}
if got, want := r.Provider, ""; got != want {
t.Errorf("wrong Resources[4].Provider %#v; want %#v", got, want)
}
wantConfig := map[string]string{}
var gotConfig map[string]string
gohcl2.DecodeBody(r.RawConfig.Body, nil, &gotConfig)
if !reflect.DeepEqual(gotConfig, wantConfig) {
t.Errorf("wrong Resources[4].RawConfig.Body %#v; want %#v", gotConfig, wantConfig)
}
}
}
// "variable" blocks
if got, want := len(cfg.Variables), 3; got != want {
t.Errorf("Variables slice has wrong length %#v; want %#v", got, want)
} else {
{
v := cfg.Variables[0]
if got, want := v.Name, "foo"; got != want {
t.Errorf("wrong Variables[0].Name %#v; want %#v", got, want)
}
if got, want := v.Default, "bar"; got != want {
t.Errorf("wrong Variables[0].Default %#v; want %#v", got, want)
}
if got, want := v.Description, "barbar"; got != want {
t.Errorf("wrong Variables[0].Description %#v; want %#v", got, want)
}
if got, want := v.DeclaredType, ""; got != want {
t.Errorf("wrong Variables[0].DeclaredType %#v; want %#v", got, want)
}
}
{
v := cfg.Variables[1]
if got, want := v.Name, "bar"; got != want {
t.Errorf("wrong Variables[1].Name %#v; want %#v", got, want)
}
if got, want := v.Default, interface{}(nil); got != want {
t.Errorf("wrong Variables[1].Default %#v; want %#v", got, want)
}
if got, want := v.Description, ""; got != want {
t.Errorf("wrong Variables[1].Description %#v; want %#v", got, want)
}
if got, want := v.DeclaredType, "string"; got != want {
t.Errorf("wrong Variables[1].DeclaredType %#v; want %#v", got, want)
}
}
{
v := cfg.Variables[2]
if got, want := v.Name, "baz"; got != want {
t.Errorf("wrong Variables[2].Name %#v; want %#v", got, want)
}
if got, want := v.Default, map[string]interface{}{"key": "value"}; !reflect.DeepEqual(got, want) {
t.Errorf("wrong Variables[2].Default %#v; want %#v", got, want)
}
if got, want := v.Description, ""; got != want {
t.Errorf("wrong Variables[2].Description %#v; want %#v", got, want)
}
if got, want := v.DeclaredType, "map"; got != want {
t.Errorf("wrong Variables[2].DeclaredType %#v; want %#v", got, want)
}
}
}
// "output" blocks
if got, want := len(cfg.Outputs), 2; got != want {
t.Errorf("Outputs slice has wrong length %#v; want %#v", got, want)
} else {
{
o := cfg.Outputs[0]
if got, want := o.Name, "web_ip"; got != want {
t.Errorf("wrong Outputs[0].Name %#v; want %#v", got, want)
}
if got, want := o.DependsOn, []string(nil); !reflect.DeepEqual(got, want) {
t.Errorf("wrong Outputs[0].DependsOn %#v; want %#v", got, want)
}
if got, want := o.Description, ""; got != want {
t.Errorf("wrong Outputs[0].Description %#v; want %#v", got, want)
}
if got, want := o.Sensitive, true; got != want {
t.Errorf("wrong Outputs[0].Sensitive %#v; want %#v", got, want)
}
wantConfig := map[string]string{
"value": "312.213.645.123",
}
var gotConfig map[string]string
ctx := &hcl2.EvalContext{
Variables: map[string]cty.Value{
"aws_instance": cty.ObjectVal(map[string]cty.Value{
"web": cty.ObjectVal(map[string]cty.Value{
"private_ip": cty.StringVal("312.213.645.123"),
}),
}),
},
}
gohcl2.DecodeBody(o.RawConfig.Body, ctx, &gotConfig)
if !reflect.DeepEqual(gotConfig, wantConfig) {
t.Errorf("wrong Outputs[0].RawConfig.Body %#v; want %#v", gotConfig, wantConfig)
}
}
{
o := cfg.Outputs[1]
if got, want := o.Name, "web_id"; got != want {
t.Errorf("wrong Outputs[1].Name %#v; want %#v", got, want)
}
if got, want := o.DependsOn, []string{"aws_instance.db"}; !reflect.DeepEqual(got, want) {
t.Errorf("wrong Outputs[1].DependsOn %#v; want %#v", got, want)
}
if got, want := o.Description, "The ID"; got != want {
t.Errorf("wrong Outputs[1].Description %#v; want %#v", got, want)
}
if got, want := o.Sensitive, false; got != want {
t.Errorf("wrong Outputs[1].Sensitive %#v; want %#v", got, want)
}
}
}
// "provider" blocks
if got, want := len(cfg.ProviderConfigs), 2; got != want {
t.Errorf("ProviderConfigs slice has wrong length %#v; want %#v", got, want)
} else {
{
p := cfg.ProviderConfigs[0]
if got, want := p.Name, "aws"; got != want {
t.Errorf("wrong ProviderConfigs[0].Name %#v; want %#v", got, want)
}
if got, want := p.Alias, ""; got != want {
t.Errorf("wrong ProviderConfigs[0].Alias %#v; want %#v", got, want)
}
if got, want := p.Version, "1.0.0"; got != want {
t.Errorf("wrong ProviderConfigs[0].Version %#v; want %#v", got, want)
}
wantConfig := map[string]string{
"access_key": "foo",
"secret_key": "bar",
}
var gotConfig map[string]string
gohcl2.DecodeBody(p.RawConfig.Body, nil, &gotConfig)
if !reflect.DeepEqual(gotConfig, wantConfig) {
t.Errorf("wrong ProviderConfigs[0].RawConfig.Body %#v; want %#v", gotConfig, wantConfig)
}
}
{
p := cfg.ProviderConfigs[1]
if got, want := p.Name, "do"; got != want {
t.Errorf("wrong ProviderConfigs[1].Name %#v; want %#v", got, want)
}
if got, want := p.Alias, "fum"; got != want {
t.Errorf("wrong ProviderConfigs[1].Alias %#v; want %#v", got, want)
}
if got, want := p.Version, ""; got != want {
t.Errorf("wrong ProviderConfigs[1].Version %#v; want %#v", got, want)
}
}
}
// "locals" definitions
if got, want := len(cfg.Locals), 5; got != want {
t.Errorf("Locals slice has wrong length %#v; want %#v", got, want)
} else {
{
l := cfg.Locals[0]
if got, want := l.Name, "security_group_ids"; got != want {
t.Errorf("wrong Locals[0].Name %#v; want %#v", got, want)
}
wantConfig := map[string][]string{
"value": []string{"sg-abc123"},
}
var gotConfig map[string][]string
ctx := &hcl2.EvalContext{
Variables: map[string]cty.Value{
"aws_security_group": cty.ObjectVal(map[string]cty.Value{
"firewall": cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("sg-abc123"),
}),
}),
},
}
gohcl2.DecodeBody(l.RawConfig.Body, ctx, &gotConfig)
if !reflect.DeepEqual(gotConfig, wantConfig) {
t.Errorf("wrong Locals[0].RawConfig.Body %#v; want %#v", gotConfig, wantConfig)
}
}
{
l := cfg.Locals[1]
if got, want := l.Name, "web_ip"; got != want {
t.Errorf("wrong Locals[1].Name %#v; want %#v", got, want)
}
}
{
l := cfg.Locals[2]
if got, want := l.Name, "literal"; got != want {
t.Errorf("wrong Locals[2].Name %#v; want %#v", got, want)
}
}
{
l := cfg.Locals[3]
if got, want := l.Name, "literal_list"; got != want {
t.Errorf("wrong Locals[3].Name %#v; want %#v", got, want)
}
}
{
l := cfg.Locals[4]
if got, want := l.Name, "literal_map"; got != want {
t.Errorf("wrong Locals[4].Name %#v; want %#v", got, want)
}
}
}
}