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.
This commit is contained in:
parent
91a8a8137c
commit
27ad9861ce
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -125,8 +125,31 @@ 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 {
|
||||
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",
|
||||
|
@ -134,6 +157,7 @@ func decodeModuleBlock(block *hcl.Block, override bool) (*ModuleCall, hcl.Diagno
|
|||
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"},
|
||||
|
|
|
@ -92,8 +92,32 @@ func decodeProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) {
|
|||
}
|
||||
}
|
||||
|
||||
// Reserved block types (all of them)
|
||||
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 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",
|
||||
|
@ -101,6 +125,7 @@ func decodeProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) {
|
|||
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"},
|
||||
},
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue