diff --git a/configs/escaping_blocks_test.go b/configs/escaping_blocks_test.go new file mode 100644 index 000000000..7996d8b77 --- /dev/null +++ b/configs/escaping_blocks_test.go @@ -0,0 +1,308 @@ +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) + } +} diff --git a/configs/module_call.go b/configs/module_call.go index 6d1678ce9..0733db854 100644 --- a/configs/module_call.go +++ b/configs/module_call.go @@ -125,14 +125,38 @@ func decodeModuleBlock(block *hcl.Block, override bool) (*ModuleCall, hcl.Diagno } } - // Reserved block types (all of them) + var seenEscapeBlock *hcl.Block for _, block := range content.Blocks { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Reserved block type name in module block", - Detail: fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type), - Subject: &block.TypeRange, - }) + switch block.Type { + case "_": + if seenEscapeBlock != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate escaping block", + Detail: fmt.Sprintf( + "The special block type \"_\" can be used to force particular arguments to be interpreted as module input variables rather than as meta-arguments, but each module block can have only one such block. The first escaping block was at %s.", + seenEscapeBlock.DefRange, + ), + Subject: &block.DefRange, + }) + continue + } + seenEscapeBlock = block + + // When there's an escaping block its content merges with the + // existing config we extracted earlier, so later decoding + // will see a blend of both. + mc.Config = hcl.MergeBodies([]hcl.Body{mc.Config, block.Body}) + + default: + // All of the other block types in our schema are reserved. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reserved block type name in module block", + Detail: fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type), + Subject: &block.TypeRange, + }) + } } return mc, diags @@ -168,6 +192,8 @@ var moduleBlockSchema = &hcl.BodySchema{ }, }, Blocks: []hcl.BlockHeaderSchema{ + {Type: "_"}, // meta-argument escaping block + // These are all reserved for future use. {Type: "lifecycle"}, {Type: "locals"}, diff --git a/configs/provider.go b/configs/provider.go index 3bbba3052..7b89a06c0 100644 --- a/configs/provider.go +++ b/configs/provider.go @@ -92,14 +92,39 @@ func decodeProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) { } } - // Reserved block types (all of them) + var seenEscapeBlock *hcl.Block for _, block := range content.Blocks { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Reserved block type name in provider block", - Detail: fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type), - Subject: &block.TypeRange, - }) + switch block.Type { + case "_": + if seenEscapeBlock != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate escaping block", + Detail: fmt.Sprintf( + "The special block type \"_\" can be used to force particular arguments to be interpreted as provider-specific rather than as meta-arguments, but each provider block can have only one such block. The first escaping block was at %s.", + seenEscapeBlock.DefRange, + ), + Subject: &block.DefRange, + }) + continue + } + seenEscapeBlock = block + + // When there's an escaping block its content merges with the + // existing config we extracted earlier, so later decoding + // will see a blend of both. + provider.Config = hcl.MergeBodies([]hcl.Body{provider.Config, block.Body}) + + default: + // All of the other block types in our schema are reserved for + // future expansion. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reserved block type name in provider block", + Detail: fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type), + Subject: &block.TypeRange, + }) + } } return provider, diags @@ -215,7 +240,9 @@ var providerBlockSchema = &hcl.BodySchema{ {Name: "source"}, }, Blocks: []hcl.BlockHeaderSchema{ - // _All_ of these are reserved for future expansion. + {Type: "_"}, // meta-argument escaping block + + // The rest of these are reserved for future expansion. {Type: "lifecycle"}, {Type: "locals"}, }, diff --git a/configs/provisioner.go b/configs/provisioner.go index 84deda3a5..4de8bf6f4 100644 --- a/configs/provisioner.go +++ b/configs/provisioner.go @@ -86,8 +86,28 @@ func decodeProvisionerBlock(block *hcl.Block) (*Provisioner, hcl.Diagnostics) { } var seenConnection *hcl.Block + var seenEscapeBlock *hcl.Block for _, block := range content.Blocks { switch block.Type { + case "_": + if seenEscapeBlock != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate escaping block", + Detail: fmt.Sprintf( + "The special block type \"_\" can be used to force particular arguments to be interpreted as provisioner-typpe-specific rather than as meta-arguments, but each provisioner block can have only one such block. The first escaping block was at %s.", + seenEscapeBlock.DefRange, + ), + Subject: &block.DefRange, + }) + continue + } + seenEscapeBlock = block + + // When there's an escaping block its content merges with the + // existing config we extracted earlier, so later decoding + // will see a blend of both. + pv.Config = hcl.MergeBodies([]hcl.Body{pv.Config, block.Body}) case "connection": if seenConnection != nil { @@ -209,6 +229,8 @@ var provisionerBlockSchema = &hcl.BodySchema{ {Name: "on_failure"}, }, Blocks: []hcl.BlockHeaderSchema{ + {Type: "_"}, // meta-argument escaping block + {Type: "connection"}, {Type: "lifecycle"}, // reserved for future use }, diff --git a/configs/resource.go b/configs/resource.go index 73c0d7c89..530048b43 100644 --- a/configs/resource.go +++ b/configs/resource.go @@ -144,6 +144,7 @@ func decodeResourceBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) { var seenLifecycle *hcl.Block var seenConnection *hcl.Block + var seenEscapeBlock *hcl.Block for _, block := range content.Blocks { switch block.Type { case "lifecycle": @@ -260,6 +261,26 @@ func decodeResourceBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) { r.Managed.Provisioners = append(r.Managed.Provisioners, pv) } + case "_": + if seenEscapeBlock != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate escaping block", + Detail: fmt.Sprintf( + "The special block type \"_\" can be used to force particular arguments to be interpreted as resource-type-specific rather than as meta-arguments, but each resource block can have only one such block. The first escaping block was at %s.", + seenEscapeBlock.DefRange, + ), + Subject: &block.DefRange, + }) + continue + } + seenEscapeBlock = block + + // When there's an escaping block its content merges with the + // existing config we extracted earlier, so later decoding + // will see a blend of both. + r.Config = hcl.MergeBodies([]hcl.Body{r.Config, block.Body}) + default: // Any other block types are ones we've reserved for future use, // so they get a generic message. @@ -346,9 +367,31 @@ func decodeDataBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) { r.DependsOn = append(r.DependsOn, deps...) } + var seenEscapeBlock *hcl.Block for _, block := range content.Blocks { - // All of the block types we accept are just reserved for future use, but some get a specialized error message. switch block.Type { + + case "_": + if seenEscapeBlock != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate escaping block", + Detail: fmt.Sprintf( + "The special block type \"_\" can be used to force particular arguments to be interpreted as resource-type-specific rather than as meta-arguments, but each data block can have only one such block. The first escaping block was at %s.", + seenEscapeBlock.DefRange, + ), + Subject: &block.DefRange, + }) + continue + } + seenEscapeBlock = block + + // When there's an escaping block its content merges with the + // existing config we extracted earlier, so later decoding + // will see a blend of both. + r.Config = hcl.MergeBodies([]hcl.Body{r.Config, block.Body}) + + // The rest of these are just here to reserve block type names for future use. case "lifecycle": diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, @@ -356,6 +399,7 @@ func decodeDataBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) { Detail: "Data resources do not have lifecycle settings, so a lifecycle block is not allowed.", Subject: &block.DefRange, }) + default: diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, @@ -500,6 +544,7 @@ var resourceBlockSchema = &hcl.BodySchema{ {Type: "lifecycle"}, {Type: "connection"}, {Type: "provisioner", LabelNames: []string{"type"}}, + {Type: "_"}, // meta-argument escaping block }, } @@ -508,6 +553,7 @@ var dataBlockSchema = &hcl.BodySchema{ Blocks: []hcl.BlockHeaderSchema{ {Type: "lifecycle"}, // reserved for future use {Type: "locals"}, // reserved for future use + {Type: "_"}, // meta-argument escaping block }, } diff --git a/configs/testdata/escaping-blocks/data/data-escaping-block.tf b/configs/testdata/escaping-blocks/data/data-escaping-block.tf new file mode 100644 index 000000000..f7efa7285 --- /dev/null +++ b/configs/testdata/escaping-blocks/data/data-escaping-block.tf @@ -0,0 +1,35 @@ + +data "foo" "bar" { + count = 2 + + normal = "yes" + + normal_block {} + + _ { + # This "escaping block" is an escape hatch for when a resource + # type declares argument names that collide with meta-argument + # names. The examples below are not really realistic because they + # are long-standing names that predate the need for escaping, + # but we're using them as a proxy for new meta-arguments we might + # add in future language editions which might collide with + # names defined in pre-existing providers. + + # note that count is set both as a meta-argument above _and_ as + # an resource-type-specific argument here, which is valid and + # should result in both being populated. + count = "not actually count" + + # for_each is only set in here, not as a meta-argument + for_each = "not actually for_each" + + lifecycle { + # This is a literal lifecycle block, not a meta-argument block + } + + _ { + # It would be pretty weird for a resource type to define its own + # "_" block type, but that's valid to escape in here too. + } + } +} diff --git a/configs/testdata/escaping-blocks/module/child/nothing.tf b/configs/testdata/escaping-blocks/module/child/nothing.tf new file mode 100644 index 000000000..e69de29bb diff --git a/configs/testdata/escaping-blocks/module/module-escaping-block.tf b/configs/testdata/escaping-blocks/module/module-escaping-block.tf new file mode 100644 index 000000000..8d8a3549f --- /dev/null +++ b/configs/testdata/escaping-blocks/module/module-escaping-block.tf @@ -0,0 +1,36 @@ + +module "foo" { + source = "./child" + count = 2 + + normal = "yes" + + normal_block {} + + _ { + # This "escaping block" is an escape hatch for when a module + # declares input variable names that collide with meta-argument + # names. The examples below are not really realistic because they + # are long-standing names that predate the need for escaping, + # but we're using them as a proxy for new meta-arguments we might + # add in future language editions which might collide with + # names defined in pre-existing modules. + + # note that count is set both as a meta-argument above _and_ as + # an resource-type-specific argument here, which is valid and + # should result in both being populated. + count = "not actually count" + + # for_each is only set in here, not as a meta-argument + for_each = "not actually for_each" + + lifecycle { + # This is a literal lifecycle block, not a meta-argument block + } + + _ { + # It would be pretty weird for a resource type to define its own + # "_" block type, but that's valid to escape in here too. + } + } +} diff --git a/configs/testdata/escaping-blocks/provider/provider-escaping-block.tf b/configs/testdata/escaping-blocks/provider/provider-escaping-block.tf new file mode 100644 index 000000000..65a11b767 --- /dev/null +++ b/configs/testdata/escaping-blocks/provider/provider-escaping-block.tf @@ -0,0 +1,23 @@ + +provider "foo" { + alias = "bar" + + normal = "yes" + + _ { + # This "escaping block" is an escape hatch for when a provider + # declares argument names that collide with meta-argument + # names. The examples below are not really realistic because they + # are long-standing names that predate the need for escaping, + # but we're using them as a proxy for new meta-arguments we might + # add in future language editions which might collide with + # names defined in pre-existing providers. + + # alias is set both as a meta-argument above _and_ + # as a provider-type-specific argument + alias = "not actually alias" + + # version is only set in here, not as a meta-argument + version = "not actually version" + } +} diff --git a/configs/testdata/escaping-blocks/resource/resource-escaping-block.tf b/configs/testdata/escaping-blocks/resource/resource-escaping-block.tf new file mode 100644 index 000000000..4d356d61b --- /dev/null +++ b/configs/testdata/escaping-blocks/resource/resource-escaping-block.tf @@ -0,0 +1,43 @@ + +resource "foo" "bar" { + count = 2 + + normal = "yes" + + normal_block {} + + _ { + # This "escaping block" is an escape hatch for when a resource + # type declares argument names that collide with meta-argument + # names. The examples below are not really realistic because they + # are long-standing names that predate the need for escaping, + # but we're using them as a proxy for new meta-arguments we might + # add in future language editions which might collide with + # names defined in pre-existing providers. + + # note that count is set both as a meta-argument above _and_ as + # an resource-type-specific argument here, which is valid and + # should result in both being populated. + count = "not actually count" + + # for_each is only set in here, not as a meta-argument + for_each = "not actually for_each" + + lifecycle { + # This is a literal lifecycle block, not a meta-argument block + } + + _ { + # It would be pretty weird for a resource type to define its own + # "_" block type, but that's valid to escape in here too. + } + } + + provisioner "boop" { + when = destroy + _ { + when = "hell freezes over" + } + normal = "yep" + } +}