package configs import ( "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" ) // "Escaping Blocks" are a special mechanism we have inside our block types // that accept a mixture of meta-arguments and externally-defined arguments, // which allow an author to force particular argument names to be interpreted // as externally-defined even if they have the same name as a meta-argument. // // An escaping block is a block with the special type name "_" (just an // underscore), and is allowed at the top-level of any resource, data, or // module block. It intentionally has a rather "odd" look so that it stands // out as something special and rare. // // This is not something we expect to see used a lot, but it's an important // part of our strategy to evolve the Terraform language in future using // editions, so that later editions can define new meta-arguments without // blocking access to externally-defined arguments of the same name. // // We should still define new meta-arguments with care to avoid squatting on // commonly-used names, but we can't see all modules and all providers in // the world and so this is an escape hatch for edge cases. Module migration // tools for future editions that define new meta-arguments should detect // collisions and automatically migrate existing arguments into an escaping // block. func TestEscapingBlockResource(t *testing.T) { // (this also tests escaping blocks in provisioner blocks, because // they only appear nested inside resource blocks.) parser := NewParser(nil) mod, diags := parser.LoadConfigDir("testdata/escaping-blocks/resource") assertNoDiagnostics(t, diags) if mod == nil { t.Fatal("got nil root module; want non-nil") } rc := mod.ManagedResources["foo.bar"] if rc == nil { t.Fatal("no managed resource named foo.bar") } t.Run("resource body", func(t *testing.T) { if got := rc.Count; got == nil { t.Errorf("count not set; want count = 2") } else { got, diags := got.Value(nil) assertNoDiagnostics(t, diags) if want := cty.NumberIntVal(2); !want.RawEquals(got) { t.Errorf("wrong count\ngot: %#v\nwant: %#v", got, want) } } if got, want := rc.ForEach, hcl.Expression(nil); got != want { // Shouldn't have any count because our test fixture only has // for_each in the escaping block. t.Errorf("wrong for_each\ngot: %#v\nwant: %#v", got, want) } schema := &hcl.BodySchema{ Attributes: []hcl.AttributeSchema{ {Name: "normal", Required: true}, {Name: "count", Required: true}, {Name: "for_each", Required: true}, }, Blocks: []hcl.BlockHeaderSchema{ {Type: "normal_block"}, {Type: "lifecycle"}, {Type: "_"}, }, } content, diags := rc.Config.Content(schema) assertNoDiagnostics(t, diags) normalVal, diags := content.Attributes["normal"].Expr.Value(nil) assertNoDiagnostics(t, diags) if got, want := normalVal, cty.StringVal("yes"); !want.RawEquals(got) { t.Errorf("wrong value for 'normal'\ngot: %#v\nwant: %#v", got, want) } countVal, diags := content.Attributes["count"].Expr.Value(nil) assertNoDiagnostics(t, diags) if got, want := countVal, cty.StringVal("not actually count"); !want.RawEquals(got) { t.Errorf("wrong value for 'count'\ngot: %#v\nwant: %#v", got, want) } var gotBlockTypes []string for _, block := range content.Blocks { gotBlockTypes = append(gotBlockTypes, block.Type) } wantBlockTypes := []string{"normal_block", "lifecycle", "_"} if diff := cmp.Diff(gotBlockTypes, wantBlockTypes); diff != "" { t.Errorf("wrong block types\n%s", diff) } }) t.Run("provisioner body", func(t *testing.T) { if got, want := len(rc.Managed.Provisioners), 1; got != want { t.Fatalf("wrong number of provisioners %d; want %d", got, want) } pc := rc.Managed.Provisioners[0] schema := &hcl.BodySchema{ Attributes: []hcl.AttributeSchema{ {Name: "when", Required: true}, {Name: "normal", Required: true}, }, Blocks: []hcl.BlockHeaderSchema{ {Type: "normal_block"}, {Type: "lifecycle"}, {Type: "_"}, }, } content, diags := pc.Config.Content(schema) assertNoDiagnostics(t, diags) normalVal, diags := content.Attributes["normal"].Expr.Value(nil) assertNoDiagnostics(t, diags) if got, want := normalVal, cty.StringVal("yep"); !want.RawEquals(got) { t.Errorf("wrong value for 'normal'\ngot: %#v\nwant: %#v", got, want) } whenVal, diags := content.Attributes["when"].Expr.Value(nil) assertNoDiagnostics(t, diags) if got, want := whenVal, cty.StringVal("hell freezes over"); !want.RawEquals(got) { t.Errorf("wrong value for 'normal'\ngot: %#v\nwant: %#v", got, want) } }) } func TestEscapingBlockData(t *testing.T) { parser := NewParser(nil) mod, diags := parser.LoadConfigDir("testdata/escaping-blocks/data") assertNoDiagnostics(t, diags) if mod == nil { t.Fatal("got nil root module; want non-nil") } rc := mod.DataResources["data.foo.bar"] if rc == nil { t.Fatal("no data resource named data.foo.bar") } if got := rc.Count; got == nil { t.Errorf("count not set; want count = 2") } else { got, diags := got.Value(nil) assertNoDiagnostics(t, diags) if want := cty.NumberIntVal(2); !want.RawEquals(got) { t.Errorf("wrong count\ngot: %#v\nwant: %#v", got, want) } } if got, want := rc.ForEach, hcl.Expression(nil); got != want { // Shouldn't have any count because our test fixture only has // for_each in the escaping block. t.Errorf("wrong for_each\ngot: %#v\nwant: %#v", got, want) } schema := &hcl.BodySchema{ Attributes: []hcl.AttributeSchema{ {Name: "normal", Required: true}, {Name: "count", Required: true}, {Name: "for_each", Required: true}, }, Blocks: []hcl.BlockHeaderSchema{ {Type: "normal_block"}, {Type: "lifecycle"}, {Type: "_"}, }, } content, diags := rc.Config.Content(schema) assertNoDiagnostics(t, diags) normalVal, diags := content.Attributes["normal"].Expr.Value(nil) assertNoDiagnostics(t, diags) if got, want := normalVal, cty.StringVal("yes"); !want.RawEquals(got) { t.Errorf("wrong value for 'normal'\ngot: %#v\nwant: %#v", got, want) } countVal, diags := content.Attributes["count"].Expr.Value(nil) assertNoDiagnostics(t, diags) if got, want := countVal, cty.StringVal("not actually count"); !want.RawEquals(got) { t.Errorf("wrong value for 'count'\ngot: %#v\nwant: %#v", got, want) } var gotBlockTypes []string for _, block := range content.Blocks { gotBlockTypes = append(gotBlockTypes, block.Type) } wantBlockTypes := []string{"normal_block", "lifecycle", "_"} if diff := cmp.Diff(gotBlockTypes, wantBlockTypes); diff != "" { t.Errorf("wrong block types\n%s", diff) } } func TestEscapingBlockModule(t *testing.T) { parser := NewParser(nil) mod, diags := parser.LoadConfigDir("testdata/escaping-blocks/module") assertNoDiagnostics(t, diags) if mod == nil { t.Fatal("got nil root module; want non-nil") } mc := mod.ModuleCalls["foo"] if mc == nil { t.Fatal("no module call named foo") } if got := mc.Count; got == nil { t.Errorf("count not set; want count = 2") } else { got, diags := got.Value(nil) assertNoDiagnostics(t, diags) if want := cty.NumberIntVal(2); !want.RawEquals(got) { t.Errorf("wrong count\ngot: %#v\nwant: %#v", got, want) } } if got, want := mc.ForEach, hcl.Expression(nil); got != want { // Shouldn't have any count because our test fixture only has // for_each in the escaping block. t.Errorf("wrong for_each\ngot: %#v\nwant: %#v", got, want) } schema := &hcl.BodySchema{ Attributes: []hcl.AttributeSchema{ {Name: "normal", Required: true}, {Name: "count", Required: true}, {Name: "for_each", Required: true}, }, Blocks: []hcl.BlockHeaderSchema{ {Type: "normal_block"}, {Type: "lifecycle"}, {Type: "_"}, }, } content, diags := mc.Config.Content(schema) assertNoDiagnostics(t, diags) normalVal, diags := content.Attributes["normal"].Expr.Value(nil) assertNoDiagnostics(t, diags) if got, want := normalVal, cty.StringVal("yes"); !want.RawEquals(got) { t.Errorf("wrong value for 'normal'\ngot: %#v\nwant: %#v", got, want) } countVal, diags := content.Attributes["count"].Expr.Value(nil) assertNoDiagnostics(t, diags) if got, want := countVal, cty.StringVal("not actually count"); !want.RawEquals(got) { t.Errorf("wrong value for 'count'\ngot: %#v\nwant: %#v", got, want) } var gotBlockTypes []string for _, block := range content.Blocks { gotBlockTypes = append(gotBlockTypes, block.Type) } wantBlockTypes := []string{"normal_block", "lifecycle", "_"} if diff := cmp.Diff(gotBlockTypes, wantBlockTypes); diff != "" { t.Errorf("wrong block types\n%s", diff) } } func TestEscapingBlockProvider(t *testing.T) { parser := NewParser(nil) mod, diags := parser.LoadConfigDir("testdata/escaping-blocks/provider") assertNoDiagnostics(t, diags) if mod == nil { t.Fatal("got nil root module; want non-nil") } pc := mod.ProviderConfigs["foo.bar"] if pc == nil { t.Fatal("no provider configuration named foo.bar") } if got, want := pc.Alias, "bar"; got != want { t.Errorf("wrong alias\ngot: %#v\nwant: %#v", got, want) } schema := &hcl.BodySchema{ Attributes: []hcl.AttributeSchema{ {Name: "normal", Required: true}, {Name: "alias", Required: true}, {Name: "version", Required: true}, }, } content, diags := pc.Config.Content(schema) assertNoDiagnostics(t, diags) normalVal, diags := content.Attributes["normal"].Expr.Value(nil) assertNoDiagnostics(t, diags) if got, want := normalVal, cty.StringVal("yes"); !want.RawEquals(got) { t.Errorf("wrong value for 'normal'\ngot: %#v\nwant: %#v", got, want) } aliasVal, diags := content.Attributes["alias"].Expr.Value(nil) assertNoDiagnostics(t, diags) if got, want := aliasVal, cty.StringVal("not actually alias"); !want.RawEquals(got) { t.Errorf("wrong value for 'alias'\ngot: %#v\nwant: %#v", got, want) } versionVal, diags := content.Attributes["version"].Expr.Value(nil) assertNoDiagnostics(t, diags) if got, want := versionVal, cty.StringVal("not actually version"); !want.RawEquals(got) { t.Errorf("wrong value for 'version'\ngot: %#v\nwant: %#v", got, want) } }