configs: Include "moved" blocks when merging multiple files into a module

An earlier commit added logic to decode "moved" blocks and do static
validation of them. Here we now include that result also in modules
produced from those files, which we can then use in Terraform Core to
actually implement the moves.

This also places the feature behind an active experiment keyword called
config_driven_move. For now activating this doesn't actually achieve
anything except let you include moved blocks that Terraform will summarily
ignore, but we'll expand the scope of this in later commits to eventually
reach the point where it's really usable.
This commit is contained in:
Martin Atkins 2021-06-30 10:46:15 -07:00
parent d92b5e5f5e
commit 6b8e103d6a
8 changed files with 99 additions and 12 deletions

View File

@ -197,6 +197,17 @@ func checkModuleExperiments(m *Module) hcl.Diagnostics {
} }
} }
if !m.ActiveExperiments.Has(experiments.ConfigDrivenMove) {
for _, mc := range m.Moved {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Config-driven move is experimental",
Detail: "This feature is currently under development and is not yet fully-functional.\n\nIf you'd like to try the partial implementation that exists so far, add config_driven_move to the set of active experiments for this module.",
Subject: mc.DeclRange.Ptr(),
})
}
}
return diags return diags
} }

View File

@ -42,6 +42,8 @@ type Module struct {
ManagedResources map[string]*Resource ManagedResources map[string]*Resource
DataResources map[string]*Resource DataResources map[string]*Resource
Moved []*Moved
} }
// File describes the contents of a single configuration file. // File describes the contents of a single configuration file.
@ -330,6 +332,11 @@ func (m *Module) appendFile(file *File) hcl.Diagnostics {
} }
} }
// "Moved" blocks just append, because they are all independent
// of one another at this level. (We handle any references between
// them at runtime.)
m.Moved = append(m.Moved, file.Moved...)
return diags return diags
} }
@ -484,6 +491,15 @@ func (m *Module) mergeFile(file *File) hcl.Diagnostics {
diags = append(diags, mergeDiags...) diags = append(diags, mergeDiags...)
} }
for _, m := range file.Moved {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Cannot override 'moved' blocks",
Detail: "Records of moved objects can appear only in normal files, not in override files.",
Subject: m.DeclRange.Ptr(),
})
}
return diags return diags
} }

View File

@ -169,6 +169,30 @@ func TestDecodeMovedBlock(t *testing.T) {
} }
} }
func TestMovedBlocksInModule(t *testing.T) {
parser := NewParser(nil)
mod, diags := parser.LoadConfigDir("testdata/valid-modules/moved-blocks")
if diags.HasErrors() {
t.Errorf("unexpected error: %s", diags.Error())
}
var gotPairs [][2]string
for _, mc := range mod.Moved {
gotPairs = append(gotPairs, [2]string{mc.From.String(), mc.To.String()})
}
wantPairs := [][2]string{
{`test.foo`, `test.bar`},
{`test.foo`, `test.bar["bloop"]`},
{`module.a`, `module.b`},
{`module.a`, `module.a["foo"]`},
{`test.foo`, `module.a.test.foo`},
{`data.test.foo`, `data.test.bar`},
}
if diff := cmp.Diff(wantPairs, gotPairs); diff != "" {
t.Errorf("wrong addresses\n%s", diff)
}
}
func mustMoveEndpointFromExpr(expr hcl.Expression) *addrs.MoveEndpoint { func mustMoveEndpointFromExpr(expr hcl.Expression) *addrs.MoveEndpoint {
traversal, hcldiags := hcl.AbsTraversalForExpr(expr) traversal, hcldiags := hcl.AbsTraversalForExpr(expr)
if hcldiags.HasErrors() { if hcldiags.HasErrors() {

View File

@ -2,7 +2,6 @@ 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
@ -150,16 +149,10 @@ func (p *Parser) loadConfigFile(path string, override bool) (*File, hcl.Diagnost
} }
case "moved": case "moved":
// This is not quite the usual usage of the experiments package. cfg, cfgDiags := decodeMovedBlock(block)
// EverythingIsAPlan is not registered as an active experiment, so diags = append(diags, cfgDiags...)
// this block will not be decoded until either the experiment is if cfg != nil {
// registered, or this check is dropped altogether. file.Moved = append(file.Moved, cfg)
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:

View File

@ -0,0 +1,7 @@
# This is currently invalid alone because config-driven move requires the
# "config_driven_move" experiment. We can remove this test case altogther
# if moved blocks of this sort graduate to being stable.
moved {
from = a.b
to = c.d
}

View File

@ -0,0 +1,29 @@
terraform {
# For the moment this is experimental
experiments = [config_driven_move]
}
moved {
from = test.foo
to = test.bar
}
moved {
from = test.foo
to = test.bar["bloop"]
}
moved {
from = module.a
to = module.b
}
moved {
from = module.a
to = module.a["foo"]
}
moved {
from = test.foo
to = module.a.test.foo
}

View File

@ -0,0 +1,6 @@
# One more moved block in a separate file just to make sure the
# appending of multiple files works properly.
moved {
from = data.test.foo
to = data.test.bar
}

View File

@ -16,7 +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") ConfigDrivenMove = Experiment("config_driven_move")
) )
func init() { func init() {
@ -25,6 +25,7 @@ func init() {
registerConcludedExperiment(VariableValidation, "Custom variable validation can now be used by default, without enabling an experiment.") registerConcludedExperiment(VariableValidation, "Custom variable validation can now be used by default, without enabling an experiment.")
registerConcludedExperiment(SuppressProviderSensitiveAttrs, "Provider-defined sensitive attributes are now redacted by default, without enabling an experiment.") registerConcludedExperiment(SuppressProviderSensitiveAttrs, "Provider-defined sensitive attributes are now redacted by default, without enabling an experiment.")
registerCurrentExperiment(ModuleVariableOptionalAttrs) registerCurrentExperiment(ModuleVariableOptionalAttrs)
registerCurrentExperiment(ConfigDrivenMove)
} }
// GetCurrent takes an experiment name and returns the experiment value // GetCurrent takes an experiment name and returns the experiment value