terraform: remove state from `validate` graph walk (#26063)
This pull reverts a recent change to backend/local which created two context, one with and one without state. Instead I have removed the state entirely from the validate graph (by explicitly passing a states.NewState() to the validate graph builder). This changed caused a test failure, which (ty so much for the help) @jbardin discovered was inaccurate all along: the test's call to `Validate()` was actually what was removing the output from state. The new expected test output matches terraform's actual behavior on the command line: if you use -target to destroy a resource, an output that references only that resource is *not* removed from state even though that test would lead you to believe it did. This includes two tests to cover the expected behavior: TestPlan_varsUnset has been updated so it will panic if it gets more than one request to input a variable TestPlan_providerArgumentUnset covers #26035 Fixes #26035, #26027
This commit is contained in:
parent
d7de46df10
commit
196c183dda
|
@ -84,13 +84,6 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, *configload.
|
||||||
log.Printf("[TRACE] backend/local: retrieving local state snapshot for workspace %q", op.Workspace)
|
log.Printf("[TRACE] backend/local: retrieving local state snapshot for workspace %q", op.Workspace)
|
||||||
opts.State = s.State()
|
opts.State = s.State()
|
||||||
|
|
||||||
// Prepare a separate opts and context for validation, which doesn't use
|
|
||||||
// any state ensuring that we only validate the config, since evaluation
|
|
||||||
// will automatically reference the state when available.
|
|
||||||
validateOpts := opts
|
|
||||||
validateOpts.State = nil
|
|
||||||
var validateCtx *terraform.Context
|
|
||||||
|
|
||||||
var tfCtx *terraform.Context
|
var tfCtx *terraform.Context
|
||||||
var ctxDiags tfdiags.Diagnostics
|
var ctxDiags tfdiags.Diagnostics
|
||||||
var configSnap *configload.Snapshot
|
var configSnap *configload.Snapshot
|
||||||
|
@ -108,18 +101,9 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, *configload.
|
||||||
// Write sources into the cache of the main loader so that they are
|
// Write sources into the cache of the main loader so that they are
|
||||||
// available if we need to generate diagnostic message snippets.
|
// available if we need to generate diagnostic message snippets.
|
||||||
op.ConfigLoader.ImportSourcesFromSnapshot(configSnap)
|
op.ConfigLoader.ImportSourcesFromSnapshot(configSnap)
|
||||||
|
|
||||||
// create a validation context with no state
|
|
||||||
validateCtx, _, _ = b.contextFromPlanFile(op.PlanFile, validateOpts, stateMeta)
|
|
||||||
// diags from here will be caught above
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[TRACE] backend/local: building context for current working directory")
|
log.Printf("[TRACE] backend/local: building context for current working directory")
|
||||||
tfCtx, configSnap, ctxDiags = b.contextDirect(op, opts)
|
tfCtx, configSnap, ctxDiags = b.contextDirect(op, opts)
|
||||||
|
|
||||||
// create a validation context with no state
|
|
||||||
validateCtx, _, _ = b.contextDirect(op, validateOpts)
|
|
||||||
// diags from here will be caught above
|
|
||||||
}
|
}
|
||||||
diags = diags.Append(ctxDiags)
|
diags = diags.Append(ctxDiags)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
|
@ -145,7 +129,7 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, *configload.
|
||||||
// If validation is enabled, validate
|
// If validation is enabled, validate
|
||||||
if b.OpValidation {
|
if b.OpValidation {
|
||||||
log.Printf("[TRACE] backend/local: running validation operation")
|
log.Printf("[TRACE] backend/local: running validation operation")
|
||||||
validateDiags := validateCtx.Validate()
|
validateDiags := tfCtx.Validate()
|
||||||
diags = diags.Append(validateDiags)
|
diags = diags.Append(validateDiags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -560,15 +560,15 @@ func TestPlan_varsUnset(t *testing.T) {
|
||||||
tmp, cwd := testCwd(t)
|
tmp, cwd := testCwd(t)
|
||||||
defer testFixCwd(t, tmp, cwd)
|
defer testFixCwd(t, tmp, cwd)
|
||||||
|
|
||||||
// Disable test mode so input would be asked
|
|
||||||
test = false
|
|
||||||
defer func() { test = true }()
|
|
||||||
|
|
||||||
// The plan command will prompt for interactive input of var.foo.
|
// The plan command will prompt for interactive input of var.foo.
|
||||||
// We'll answer "bar" to that prompt, which should then allow this
|
// We'll answer "bar" to that prompt, which should then allow this
|
||||||
// configuration to apply even though var.foo doesn't have a
|
// configuration to apply even though var.foo doesn't have a
|
||||||
// default value and there are no -var arguments on our command line.
|
// default value and there are no -var arguments on our command line.
|
||||||
defaultInputReader = bytes.NewBufferString("bar\n")
|
|
||||||
|
// This will (helpfully) panic if more than one variable is requested during plan:
|
||||||
|
// https://github.com/hashicorp/terraform/issues/26027
|
||||||
|
close := testInteractiveInput(t, []string{"bar"})
|
||||||
|
defer close()
|
||||||
|
|
||||||
p := planVarsFixtureProvider()
|
p := planVarsFixtureProvider()
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
|
@ -587,6 +587,64 @@ func TestPlan_varsUnset(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This test adds a required argument to the test provider to validate
|
||||||
|
// processing of user input:
|
||||||
|
// https://github.com/hashicorp/terraform/issues/26035
|
||||||
|
func TestPlan_providerArgumentUnset(t *testing.T) {
|
||||||
|
tmp, cwd := testCwd(t)
|
||||||
|
defer testFixCwd(t, tmp, cwd)
|
||||||
|
|
||||||
|
// Disable test mode so input would be asked
|
||||||
|
test = false
|
||||||
|
defer func() { test = true }()
|
||||||
|
|
||||||
|
// The plan command will prompt for interactive input of provider.test.region
|
||||||
|
defaultInputReader = bytes.NewBufferString("us-east-1\n")
|
||||||
|
|
||||||
|
p := planFixtureProvider()
|
||||||
|
// override the planFixtureProvider schema to include a required provider argument
|
||||||
|
p.GetSchemaReturn = &terraform.ProviderSchema{
|
||||||
|
Provider: &configschema.Block{
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"region": {Type: cty.String, Required: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ResourceTypes: map[string]*configschema.Block{
|
||||||
|
"test_instance": {
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||||
|
"ami": {Type: cty.String, Optional: true, Computed: true},
|
||||||
|
},
|
||||||
|
BlockTypes: map[string]*configschema.NestedBlock{
|
||||||
|
"network_interface": {
|
||||||
|
Nesting: configschema.NestingList,
|
||||||
|
Block: configschema.Block{
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"device_index": {Type: cty.String, Optional: true},
|
||||||
|
"description": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &PlanCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
testingOverrides: metaOverridesForProvider(p),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
testFixturePath("plan"),
|
||||||
|
}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPlan_varFile(t *testing.T) {
|
func TestPlan_varFile(t *testing.T) {
|
||||||
tmp, cwd := testCwd(t)
|
tmp, cwd := testCwd(t)
|
||||||
defer testFixCwd(t, tmp, cwd)
|
defer testFixCwd(t, tmp, cwd)
|
||||||
|
|
|
@ -264,27 +264,27 @@ func (c *Context) Graph(typ GraphType, opts *ContextGraphOpts) (*Graph, tfdiags.
|
||||||
}).Build(addrs.RootModuleInstance)
|
}).Build(addrs.RootModuleInstance)
|
||||||
|
|
||||||
case GraphTypeValidate:
|
case GraphTypeValidate:
|
||||||
// The validate graph is just a slightly modified plan graph
|
// The validate graph is just a slightly modified plan graph: an empty
|
||||||
fallthrough
|
// state is substituted in for Validate.
|
||||||
|
return ValidateGraphBuilder(&PlanGraphBuilder{
|
||||||
|
Config: c.config,
|
||||||
|
Components: c.components,
|
||||||
|
Schemas: c.schemas,
|
||||||
|
Targets: c.targets,
|
||||||
|
Validate: opts.Validate,
|
||||||
|
State: states.NewState(),
|
||||||
|
}).Build(addrs.RootModuleInstance)
|
||||||
|
|
||||||
case GraphTypePlan:
|
case GraphTypePlan:
|
||||||
// Create the plan graph builder
|
// Create the plan graph builder
|
||||||
p := &PlanGraphBuilder{
|
return (&PlanGraphBuilder{
|
||||||
Config: c.config,
|
Config: c.config,
|
||||||
State: c.state,
|
State: c.state,
|
||||||
Components: c.components,
|
Components: c.components,
|
||||||
Schemas: c.schemas,
|
Schemas: c.schemas,
|
||||||
Targets: c.targets,
|
Targets: c.targets,
|
||||||
Validate: opts.Validate,
|
Validate: opts.Validate,
|
||||||
}
|
}).Build(addrs.RootModuleInstance)
|
||||||
|
|
||||||
// Some special cases for other graph types shared with plan currently
|
|
||||||
var b GraphBuilder = p
|
|
||||||
switch typ {
|
|
||||||
case GraphTypeValidate:
|
|
||||||
b = ValidateGraphBuilder(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.Build(addrs.RootModuleInstance)
|
|
||||||
|
|
||||||
case GraphTypePlanDestroy:
|
case GraphTypePlanDestroy:
|
||||||
return (&DestroyPlanGraphBuilder{
|
return (&DestroyPlanGraphBuilder{
|
||||||
|
@ -769,6 +769,17 @@ func (c *Context) walk(graph *Graph, operation walkOperation) (*ContextGraphWalk
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) graphWalker(operation walkOperation) *ContextGraphWalker {
|
func (c *Context) graphWalker(operation walkOperation) *ContextGraphWalker {
|
||||||
|
if operation == walkValidate {
|
||||||
|
return &ContextGraphWalker{
|
||||||
|
Context: c,
|
||||||
|
State: states.NewState().SyncWrapper(),
|
||||||
|
Changes: c.changes.SyncWrapper(),
|
||||||
|
InstanceExpander: instances.NewExpander(),
|
||||||
|
Operation: operation,
|
||||||
|
StopContext: c.runContext,
|
||||||
|
RootVariableValues: c.variables,
|
||||||
|
}
|
||||||
|
}
|
||||||
return &ContextGraphWalker{
|
return &ContextGraphWalker{
|
||||||
Context: c,
|
Context: c,
|
||||||
State: c.state.SyncWrapper(),
|
State: c.state.SyncWrapper(),
|
||||||
|
|
|
@ -7461,10 +7461,19 @@ func TestContext2Apply_targetedDestroy(t *testing.T) {
|
||||||
t.Fatalf("expected 0 resources, got: %#v", mod.Resources)
|
t.Fatalf("expected 0 resources, got: %#v", mod.Resources)
|
||||||
}
|
}
|
||||||
|
|
||||||
// the root output should have been removed too, since it is derived solely
|
// the root output should not get removed; only the targeted resource.
|
||||||
// from the targeted resource
|
//
|
||||||
if len(mod.OutputValues) != 0 {
|
// Note: earlier versions of this test expected 0 outputs, but it turns out
|
||||||
t.Fatalf("expected 0 outputs, got: %#v", mod.OutputValues)
|
// that was because Validate - not apply or destroy - removed the output
|
||||||
|
// (which depends on the targeted resource) from state. That version of this
|
||||||
|
// test did not match actual terraform behavior: the output remains in
|
||||||
|
// state.
|
||||||
|
//
|
||||||
|
// TODO: Future refactoring may enable us to remove the output from state in
|
||||||
|
// this case, and that would be Just Fine - this test can be modified to
|
||||||
|
// expect 0 outputs.
|
||||||
|
if len(mod.OutputValues) != 1 {
|
||||||
|
t.Fatalf("expected 1 outputs, got: %#v", mod.OutputValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
// the module instance should remain
|
// the module instance should remain
|
||||||
|
|
Loading…
Reference in New Issue