Skip to content

Commit 4afc3d7

Browse files
committed
Allow use of backend block to set initial state for a state key
1 parent a8c57d1 commit 4afc3d7

File tree

4 files changed

+99
-14
lines changed

4 files changed

+99
-14
lines changed

internal/backend/local/test.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"github.com/zclconf/go-cty/cty"
1515

16+
"github.com/hashicorp/terraform/internal/backend"
1617
"github.com/hashicorp/terraform/internal/backend/backendrun"
1718
"github.com/hashicorp/terraform/internal/command/junit"
1819
"github.com/hashicorp/terraform/internal/command/views"
@@ -29,6 +30,15 @@ import (
2930
type TestSuiteRunner struct {
3031
Config *configs.Config
3132

33+
// BackendFactory is used to enable initialising multiple backend types,
34+
// depending on which backends are used in a test suite.
35+
//
36+
// Note: This is currently necessary because the source of the init functions,
37+
// the backend/init package, experiences import cycles if used in other test-related
38+
// packages. We set this field on a TestSuiteRunner when making runners in the
39+
// command package, which is the main place where backend/init has previously been used.
40+
BackendFactory func(string) backend.InitFn
41+
3242
TestingDirectory string
3343

3444
// Global variables comes from the main configuration directory,
@@ -270,9 +280,10 @@ func (runner *TestFileRunner) Test(file *moduletest.File) {
270280

271281
// Build the graph for the file.
272282
b := graph.TestGraphBuilder{
273-
File: file,
274-
GlobalVars: runner.EvalContext.VariableCaches.GlobalVariables,
275-
ContextOpts: runner.Suite.Opts,
283+
File: file,
284+
GlobalVars: runner.EvalContext.VariableCaches.GlobalVariables,
285+
ContextOpts: runner.Suite.Opts,
286+
BackendFactory: runner.Suite.BackendFactory,
276287
}
277288
g, diags := b.Build()
278289
file.Diagnostics = file.Diagnostics.Append(diags)

internal/command/test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,8 @@ func (c *TestCommand) Run(rawArgs []string) int {
260260
}
261261
} else {
262262
localRunner := &local.TestSuiteRunner{
263-
Config: config,
263+
BackendFactory: backendInit.Backend,
264+
Config: config,
264265
// The GlobalVariables are loaded from the
265266
// main configuration directory
266267
// The GlobalTestVariables are loaded from the

internal/moduletest/graph/test_graph_builder.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"log"
88

99
"github.com/hashicorp/terraform/internal/addrs"
10+
"github.com/hashicorp/terraform/internal/backend"
1011
"github.com/hashicorp/terraform/internal/backend/backendrun"
1112
"github.com/hashicorp/terraform/internal/dag"
1213
"github.com/hashicorp/terraform/internal/moduletest"
@@ -18,9 +19,10 @@ import (
1819
// a terraform test file. The file may contain multiple runs, and each run may have
1920
// dependencies on other runs.
2021
type TestGraphBuilder struct {
21-
File *moduletest.File
22-
GlobalVars map[string]backendrun.UnparsedVariableValue
23-
ContextOpts *terraform.ContextOpts
22+
File *moduletest.File
23+
GlobalVars map[string]backendrun.UnparsedVariableValue
24+
ContextOpts *terraform.ContextOpts
25+
BackendFactory func(string) backend.InitFn
2426
}
2527

2628
type graphOptions struct {
@@ -47,7 +49,9 @@ func (b *TestGraphBuilder) Steps() []terraform.GraphTransformer {
4749
}
4850
steps := []terraform.GraphTransformer{
4951
&TestRunTransformer{opts},
50-
&TestStateTransformer{File: b.File},
52+
// Could setting initial state based on backends be better-implemented as
53+
// another transformer?
54+
&TestStateTransformer{File: b.File, BackendFactory: b.BackendFactory},
5155
&TestStateCleanupTransformer{opts},
5256
terraform.DynamicTransformer(validateRunConfigs),
5357
&TestProvidersTransformer{},

internal/moduletest/graph/transform_state.go

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ package graph
55

66
import (
77
"fmt"
8+
"log"
89
"maps"
910

1011
"github.com/hashicorp/hcl/v2"
12+
"github.com/hashicorp/hcl/v2/hcldec"
13+
"github.com/hashicorp/terraform/internal/backend"
1114
"github.com/hashicorp/terraform/internal/configs"
1215
"github.com/hashicorp/terraform/internal/dag"
1316
"github.com/hashicorp/terraform/internal/moduletest"
@@ -32,7 +35,8 @@ type TestFileState struct {
3235
// TestStateTransformer is a GraphTransformer that initializes the context with
3336
// all the states produced by the test file.
3437
type TestStateTransformer struct {
35-
File *moduletest.File
38+
File *moduletest.File
39+
BackendFactory func(string) backend.InitFn
3640
}
3741

3842
func (t *TestStateTransformer) Transform(g *terraform.Graph) error {
@@ -48,11 +52,44 @@ func (t *TestStateTransformer) Transform(g *terraform.Graph) error {
4852
for node := range dag.SelectSeq(g.VerticesSeq(), runFilter) {
4953
key := node.run.Config.StateKey
5054
if _, exists := statesMap[key]; !exists {
51-
state := &TestFileState{
52-
File: t.File,
53-
Run: nil,
54-
State: states.NewState(),
55+
56+
var state *TestFileState
57+
58+
if bc, exists := t.File.Config.BackendConfigs[key]; exists {
59+
// If the state for that state key should come from a backend,
60+
// obtain and use that
61+
if t.BackendFactory == nil {
62+
return fmt.Errorf("error retrieving state for state key %q from backend: nil BackendFactory. This is a bug in Terraform and should be reported.", key)
63+
}
64+
65+
f := t.BackendFactory(bc.Backend.Type)
66+
if f == nil {
67+
return fmt.Errorf("error retrieving state for state key %q from backend: No init function found for backend type %q. This is a bug in Terraform and should be reported.", key, bc.Backend.Type)
68+
}
69+
be, err := getBackendInstance(key, bc.Backend, f)
70+
if err != nil {
71+
return err
72+
}
73+
74+
stmgr, err := be.StateMgr(backend.DefaultStateName) // We only allow use of the default workspace
75+
if err != nil {
76+
return fmt.Errorf("error retrieving state for state key %q from backend: error retrieving state manager: %w", key, err)
77+
}
78+
79+
log.Printf("[TRACE] TestConfigTransformer.Transform: set initial state for state key %q using backend of type %T declared at %s", key, be, bc.Backend.DeclRange)
80+
state = &TestFileState{
81+
Run: nil,
82+
State: stmgr.State(),
83+
}
84+
} else {
85+
// Else, set an empty in-memory state for the state key
86+
log.Printf("[TRACE] TestConfigTransformer.Transform: set initial state for state key %q as empty state", key)
87+
state = &TestFileState{
88+
Run: nil,
89+
State: states.NewState(),
90+
}
5591
}
92+
5693
statesMap[key] = state
5794
}
5895

@@ -64,7 +101,39 @@ func (t *TestStateTransformer) Transform(g *terraform.Graph) error {
64101
return nil
65102
}
66103

67-
func (t *TestStateTransformer) addRootConfigNode(g *terraform.Graph, statesMap map[string]*TestFileState) *dynamicNode {
104+
// getBackendInstance uses the config for a given run block's backend block to create and return a configured
105+
// instance of that backend type.
106+
func getBackendInstance(stateKey string, config *configs.Backend, f backend.InitFn) (backend.Backend, error) {
107+
b := f()
108+
log.Printf("[TRACE] TestConfigTransformer.Transform: instantiated backend of type %T", b)
109+
110+
schema := b.ConfigSchema()
111+
decSpec := schema.NoneRequired().DecoderSpec()
112+
configVal, hclDiags := hcldec.Decode(config.Config, decSpec, nil)
113+
if hclDiags.HasErrors() {
114+
return nil, fmt.Errorf("error decoding backend configuration for state key %s : %v", stateKey, hclDiags.Errs())
115+
}
116+
117+
if !configVal.IsWhollyKnown() {
118+
return nil, fmt.Errorf("unknown values within backend definition for state key %s", stateKey)
119+
}
120+
121+
newVal, validateDiags := b.PrepareConfig(configVal)
122+
validateDiags = validateDiags.InConfigBody(config.Config, "")
123+
if validateDiags.HasErrors() {
124+
return nil, validateDiags.Err()
125+
}
126+
127+
configureDiags := b.Configure(newVal)
128+
configureDiags = configureDiags.InConfigBody(config.Config, "")
129+
if validateDiags.HasErrors() {
130+
return nil, configureDiags.Err()
131+
}
132+
133+
return b, nil
134+
}
135+
136+
func (t *TestConfigTransformer) addRootConfigNode(g *terraform.Graph, statesMap map[string]*TestFileState) *dynamicNode {
68137
rootConfigNode := &dynamicNode{
69138
eval: func(ctx *EvalContext) tfdiags.Diagnostics {
70139
var diags tfdiags.Diagnostics

0 commit comments

Comments
 (0)