configs: add decodeMovedBlock behind a locked gate. (#28973)
This PR adds decoding for the upcoming "moved" blocks in configuration. This code is gated behind an experiment called EverythingIsAPlan, but the experiment is not registered as an active experiment, so it will never run (there is a test in place which will fail if the experiment is ever registered). This also adds a new function to the Targetable interface, AddrType, to simplifying comparing two addrs.Targetable. There is some validation missing still: this does not (yet) descend into resources to see if the actual resource types are the same (I've put this off in part because we will eventually need the provider schema to verify aliased resources, so I suspect this validation will have to happen later on).
This commit is contained in:
parent
bb868606ea
commit
3acb5e2841
|
@ -95,6 +95,10 @@ func (m Module) TargetContains(other Targetable) bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Module) AddrType() TargetableAddrType {
|
||||||
|
return ModuleAddrType
|
||||||
|
}
|
||||||
|
|
||||||
// Child returns the address of a child call in the receiver, identified by the
|
// Child returns the address of a child call in the receiver, identified by the
|
||||||
// given name.
|
// given name.
|
||||||
func (m Module) Child(name string) Module {
|
func (m Module) Child(name string) Module {
|
||||||
|
|
|
@ -484,6 +484,10 @@ func (m ModuleInstance) Module() Module {
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m ModuleInstance) AddrType() TargetableAddrType {
|
||||||
|
return ModuleInstanceAddrType
|
||||||
|
}
|
||||||
|
|
||||||
func (m ModuleInstance) targetableSigil() {
|
func (m ModuleInstance) targetableSigil() {
|
||||||
// ModuleInstance is targetable
|
// ModuleInstance is targetable
|
||||||
}
|
}
|
||||||
|
|
|
@ -163,6 +163,10 @@ func (r AbsResource) TargetContains(other Targetable) bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r AbsResource) AddrType() TargetableAddrType {
|
||||||
|
return AbsResourceAddrType
|
||||||
|
}
|
||||||
|
|
||||||
func (r AbsResource) String() string {
|
func (r AbsResource) String() string {
|
||||||
if len(r.Module) == 0 {
|
if len(r.Module) == 0 {
|
||||||
return r.Resource.String()
|
return r.Resource.String()
|
||||||
|
@ -228,6 +232,10 @@ func (r AbsResourceInstance) TargetContains(other Targetable) bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r AbsResourceInstance) AddrType() TargetableAddrType {
|
||||||
|
return AbsResourceInstanceAddrType
|
||||||
|
}
|
||||||
|
|
||||||
func (r AbsResourceInstance) String() string {
|
func (r AbsResourceInstance) String() string {
|
||||||
if len(r.Module) == 0 {
|
if len(r.Module) == 0 {
|
||||||
return r.Resource.String()
|
return r.Resource.String()
|
||||||
|
@ -312,6 +320,10 @@ func (r ConfigResource) TargetContains(other Targetable) bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r ConfigResource) AddrType() TargetableAddrType {
|
||||||
|
return ConfigResourceAddrType
|
||||||
|
}
|
||||||
|
|
||||||
func (r ConfigResource) String() string {
|
func (r ConfigResource) String() string {
|
||||||
if len(r.Module) == 0 {
|
if len(r.Module) == 0 {
|
||||||
return r.Resource.String()
|
return r.Resource.String()
|
||||||
|
|
|
@ -13,6 +13,10 @@ type Targetable interface {
|
||||||
// A targetable address always contains at least itself.
|
// A targetable address always contains at least itself.
|
||||||
TargetContains(other Targetable) bool
|
TargetContains(other Targetable) bool
|
||||||
|
|
||||||
|
// AddrType returns the address type for comparison with other Targetable
|
||||||
|
// addresses.
|
||||||
|
AddrType() TargetableAddrType
|
||||||
|
|
||||||
// String produces a string representation of the address that could be
|
// String produces a string representation of the address that could be
|
||||||
// parsed as a HCL traversal and passed to ParseTarget to produce an
|
// parsed as a HCL traversal and passed to ParseTarget to produce an
|
||||||
// identical result.
|
// identical result.
|
||||||
|
@ -24,3 +28,13 @@ type targetable struct {
|
||||||
|
|
||||||
func (r targetable) targetableSigil() {
|
func (r targetable) targetableSigil() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TargetableAddrType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConfigResourceAddrType TargetableAddrType = iota
|
||||||
|
AbsResourceInstanceAddrType
|
||||||
|
AbsResourceAddrType
|
||||||
|
ModuleAddrType
|
||||||
|
ModuleInstanceAddrType
|
||||||
|
)
|
||||||
|
|
|
@ -73,6 +73,8 @@ type File struct {
|
||||||
|
|
||||||
ManagedResources []*Resource
|
ManagedResources []*Resource
|
||||||
DataResources []*Resource
|
DataResources []*Resource
|
||||||
|
|
||||||
|
Moved []*Moved
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewModule takes a list of primary files and a list of override files and
|
// NewModule takes a list of primary files and a list of override files and
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
package configs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/hcl/v2"
|
||||||
|
"github.com/hashicorp/terraform/internal/addrs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Moved struct {
|
||||||
|
From *addrs.Target
|
||||||
|
To *addrs.Target
|
||||||
|
|
||||||
|
DeclRange hcl.Range
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeMovedBlock(block *hcl.Block) (*Moved, hcl.Diagnostics) {
|
||||||
|
var diags hcl.Diagnostics
|
||||||
|
moved := &Moved{
|
||||||
|
DeclRange: block.DefRange,
|
||||||
|
}
|
||||||
|
|
||||||
|
content, moreDiags := block.Body.Content(movedBlockSchema)
|
||||||
|
diags = append(diags, moreDiags...)
|
||||||
|
|
||||||
|
if attr, exists := content.Attributes["from"]; exists {
|
||||||
|
from, traversalDiags := hcl.AbsTraversalForExpr(attr.Expr)
|
||||||
|
diags = append(diags, traversalDiags...)
|
||||||
|
if !traversalDiags.HasErrors() {
|
||||||
|
from, fromDiags := addrs.ParseTarget(from)
|
||||||
|
diags = append(diags, fromDiags.ToHCL()...)
|
||||||
|
moved.From = from
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if attr, exists := content.Attributes["to"]; exists {
|
||||||
|
to, traversalDiags := hcl.AbsTraversalForExpr(attr.Expr)
|
||||||
|
diags = append(diags, traversalDiags...)
|
||||||
|
if !traversalDiags.HasErrors() {
|
||||||
|
to, toDiags := addrs.ParseTarget(to)
|
||||||
|
diags = append(diags, toDiags.ToHCL()...)
|
||||||
|
moved.To = to
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we can only move from a module to a module, resource to resource, etc.
|
||||||
|
if !diags.HasErrors() {
|
||||||
|
if moved.To.Subject.AddrType() != moved.From.Subject.AddrType() {
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Invalid \"moved\" targets",
|
||||||
|
Detail: "The \"from\" and \"to\" targets must be the same address type",
|
||||||
|
Subject: &moved.DeclRange,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return moved, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
var movedBlockSchema = &hcl.BodySchema{
|
||||||
|
Attributes: []hcl.AttributeSchema{
|
||||||
|
{
|
||||||
|
Name: "from",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "to",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,184 @@
|
||||||
|
package configs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/hashicorp/hcl/v2"
|
||||||
|
"github.com/hashicorp/hcl/v2/hcltest"
|
||||||
|
"github.com/hashicorp/terraform/internal/addrs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecodeMovedBlock(t *testing.T) {
|
||||||
|
blockRange := hcl.Range{
|
||||||
|
Filename: "mock.tf",
|
||||||
|
Start: hcl.Pos{Line: 3, Column: 12, Byte: 27},
|
||||||
|
End: hcl.Pos{Line: 3, Column: 19, Byte: 34},
|
||||||
|
}
|
||||||
|
|
||||||
|
foo_expr := hcltest.MockExprTraversalSrc("test_instance.foo")
|
||||||
|
bar_expr := hcltest.MockExprTraversalSrc("test_instance.bar")
|
||||||
|
|
||||||
|
foo_index_expr := hcltest.MockExprTraversalSrc("test_instance.foo[1]")
|
||||||
|
bar_index_expr := hcltest.MockExprTraversalSrc("test_instance.bar[\"one\"]")
|
||||||
|
|
||||||
|
mod_foo_expr := hcltest.MockExprTraversalSrc("module.foo")
|
||||||
|
mod_bar_expr := hcltest.MockExprTraversalSrc("module.bar")
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
input *hcl.Block
|
||||||
|
want *Moved
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
"success": {
|
||||||
|
&hcl.Block{
|
||||||
|
Type: "moved",
|
||||||
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
||||||
|
Attributes: hcl.Attributes{
|
||||||
|
"from": {
|
||||||
|
Name: "from",
|
||||||
|
Expr: foo_expr,
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
Name: "to",
|
||||||
|
Expr: bar_expr,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
DefRange: blockRange,
|
||||||
|
},
|
||||||
|
&Moved{
|
||||||
|
From: mustTargetFromExpr(foo_expr),
|
||||||
|
To: mustTargetFromExpr(bar_expr),
|
||||||
|
DeclRange: blockRange,
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
"indexed resources": {
|
||||||
|
&hcl.Block{
|
||||||
|
Type: "moved",
|
||||||
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
||||||
|
Attributes: hcl.Attributes{
|
||||||
|
"from": {
|
||||||
|
Name: "from",
|
||||||
|
Expr: foo_index_expr,
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
Name: "to",
|
||||||
|
Expr: bar_index_expr,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
DefRange: blockRange,
|
||||||
|
},
|
||||||
|
&Moved{
|
||||||
|
From: mustTargetFromExpr(foo_index_expr),
|
||||||
|
To: mustTargetFromExpr(bar_index_expr),
|
||||||
|
DeclRange: blockRange,
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
"modules": {
|
||||||
|
&hcl.Block{
|
||||||
|
Type: "moved",
|
||||||
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
||||||
|
Attributes: hcl.Attributes{
|
||||||
|
"from": {
|
||||||
|
Name: "from",
|
||||||
|
Expr: mod_foo_expr,
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
Name: "to",
|
||||||
|
Expr: mod_bar_expr,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
DefRange: blockRange,
|
||||||
|
},
|
||||||
|
&Moved{
|
||||||
|
From: mustTargetFromExpr(mod_foo_expr),
|
||||||
|
To: mustTargetFromExpr(mod_bar_expr),
|
||||||
|
DeclRange: blockRange,
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
"error: missing argument": {
|
||||||
|
&hcl.Block{
|
||||||
|
Type: "moved",
|
||||||
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
||||||
|
Attributes: hcl.Attributes{
|
||||||
|
"from": {
|
||||||
|
Name: "from",
|
||||||
|
Expr: foo_expr,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
DefRange: blockRange,
|
||||||
|
},
|
||||||
|
&Moved{
|
||||||
|
From: mustTargetFromExpr(foo_expr),
|
||||||
|
DeclRange: blockRange,
|
||||||
|
},
|
||||||
|
"Missing required argument",
|
||||||
|
},
|
||||||
|
"error: type mismatch": {
|
||||||
|
&hcl.Block{
|
||||||
|
Type: "moved",
|
||||||
|
Body: hcltest.MockBody(&hcl.BodyContent{
|
||||||
|
Attributes: hcl.Attributes{
|
||||||
|
"to": {
|
||||||
|
Name: "to",
|
||||||
|
Expr: foo_expr,
|
||||||
|
},
|
||||||
|
"from": {
|
||||||
|
Name: "from",
|
||||||
|
Expr: mod_foo_expr,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
DefRange: blockRange,
|
||||||
|
},
|
||||||
|
&Moved{
|
||||||
|
To: mustTargetFromExpr(foo_expr),
|
||||||
|
From: mustTargetFromExpr(mod_foo_expr),
|
||||||
|
DeclRange: blockRange,
|
||||||
|
},
|
||||||
|
"Invalid \"moved\" targets",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
got, diags := decodeMovedBlock(test.input)
|
||||||
|
|
||||||
|
if diags.HasErrors() {
|
||||||
|
if test.err == "" {
|
||||||
|
t.Fatalf("unexpected error: %s", diags.Errs())
|
||||||
|
}
|
||||||
|
if gotErr := diags[0].Summary; gotErr != test.err {
|
||||||
|
t.Errorf("wrong error, got %q, want %q", gotErr, test.err)
|
||||||
|
}
|
||||||
|
} else if test.err != "" {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cmp.Equal(got, test.want) {
|
||||||
|
t.Fatalf("wrong result: %s", cmp.Diff(got, test.want))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustTargetFromExpr(expr hcl.Expression) *addrs.Target {
|
||||||
|
traversal, hcldiags := hcl.AbsTraversalForExpr(expr)
|
||||||
|
if hcldiags.HasErrors() {
|
||||||
|
panic(hcldiags.Errs())
|
||||||
|
}
|
||||||
|
|
||||||
|
target, diags := addrs.ParseTarget(traversal)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
panic(diags.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
return target
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package configs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
|
"github.com/hashicorp/terraform/internal/experiments"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LoadConfigFile reads the file at the given path and parses it as a config
|
// LoadConfigFile reads the file at the given path and parses it as a config
|
||||||
|
@ -148,6 +149,19 @@ func (p *Parser) loadConfigFile(path string, override bool) (*File, hcl.Diagnost
|
||||||
file.DataResources = append(file.DataResources, cfg)
|
file.DataResources = append(file.DataResources, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "moved":
|
||||||
|
// This is not quite the usual usage of the experiments package.
|
||||||
|
// EverythingIsAPlan is not registered as an active experiment, so
|
||||||
|
// this block will not be decoded until either the experiment is
|
||||||
|
// registered, or this check is dropped altogether.
|
||||||
|
if file.ActiveExperiments.Has(experiments.EverythingIsAPlan) {
|
||||||
|
cfg, cfgDiags := decodeMovedBlock(block)
|
||||||
|
diags = append(diags, cfgDiags...)
|
||||||
|
if cfg != nil {
|
||||||
|
file.Moved = append(file.Moved, cfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Should never happen because the above cases should be exhaustive
|
// Should never happen because the above cases should be exhaustive
|
||||||
// for all block type names in our schema.
|
// for all block type names in our schema.
|
||||||
|
@ -235,6 +249,9 @@ var configFileSchema = &hcl.BodySchema{
|
||||||
Type: "data",
|
Type: "data",
|
||||||
LabelNames: []string{"type", "name"},
|
LabelNames: []string{"type", "name"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Type: "moved",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
# experiments.EverythingIsAPlan exists but is not registered as an active (or
|
||||||
|
# concluded) experiment, so this should fail until the experiment "gate" is
|
||||||
|
# removed.
|
||||||
|
terraform {
|
||||||
|
experiments = [everything_is_a_plan]
|
||||||
|
}
|
||||||
|
|
||||||
|
moved {
|
||||||
|
from = test_instance.foo
|
||||||
|
to = test_instance.bar
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ const (
|
||||||
VariableValidation = Experiment("variable_validation")
|
VariableValidation = Experiment("variable_validation")
|
||||||
ModuleVariableOptionalAttrs = Experiment("module_variable_optional_attrs")
|
ModuleVariableOptionalAttrs = Experiment("module_variable_optional_attrs")
|
||||||
SuppressProviderSensitiveAttrs = Experiment("provider_sensitive_attrs")
|
SuppressProviderSensitiveAttrs = Experiment("provider_sensitive_attrs")
|
||||||
|
EverythingIsAPlan = Experiment("everything_is_a_plan")
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
Loading…
Reference in New Issue