terraform/configs/escaping_blocks_test.go

309 lines
10 KiB
Go
Raw Normal View History

configs: Meta-argument escaping blocks Several top-level block types in the Terraform language have a body where two different schemas are overlayed on top of one another: Terraform first looks for "meta-arguments" that are built into the language, and then evaluates all of the remaining arguments against some externally-defined schema whose content is not fully controlled by Terraform. So far we've been cautiously adding new meta-arguments in these namespaces after research shows us that there are relatively few existing providers or modules that would have functionality masked by those additions, but that isn't really a viable path forward as we prepare to make stronger compatibility promises. In an earlier commit we've introduced the foundational parts of a new language versioning mechanism called "editions" which should allow us to make per-module-opt-in breaking changes in the future, but these shared namespaces remain a liability because it would be annoying if adopting a new edition made it impossible to use a feature of a third-party provider or module that was already using a name that has now become reserved in the new edition. This commit introduces a new syntax intended to be a rarely-used escape hatch for that situation. When we're designing new editions we will do our best to choose names that don't conflict with commonly-used providers and modules, but there are many providers and modules that we cannot see and so there is a risk that any name we might choose could collide with at least one existing provider or module. The automatic migration tool to upgrade an existing module to a new edition should therefore detect that situation and make use of this escaping block syntax in order to retain the existing functionality until all the called providers or modules are updated to no longer use conflicting names. Although we can't put in technical constraints on using this feature for other purposes (because we don't know yet what future editions will add), this mechanism is intentionally not documented for now because it serves no immediate purpose. In effect, this change is just squatting on the syntax of a special block type named "_" so that later editions can make use of it without it _also_ conflicting, creating a confusing nested escaping situation. However, the first time a new edition actually makes use of this syntax we should then document alongside the meta-arguments so folks can understand the meaning of escaping blocks produced by edition upgrade tools.
2021-05-15 01:09:51 +02:00
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)
}
}