diff --git a/internal/backend/local/test.go b/internal/backend/local/test.go index b83642db6f..5a3d926fc5 100644 --- a/internal/backend/local/test.go +++ b/internal/backend/local/test.go @@ -13,6 +13,7 @@ import ( "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/command/junit" "github.com/hashicorp/terraform/internal/command/views" @@ -29,6 +30,15 @@ import ( type TestSuiteRunner struct { Config *configs.Config + // BackendFactory is used to enable initialising multiple backend types, + // depending on which backends are used in a test suite. + // + // Note: This is currently necessary because the source of the init functions, + // the backend/init package, experiences import cycles if used in other test-related + // packages. We set this field on a TestSuiteRunner when making runners in the + // command package, which is the main place where backend/init has previously been used. + BackendFactory func(string) backend.InitFn + TestingDirectory string // Global variables comes from the main configuration directory, @@ -270,9 +280,10 @@ func (runner *TestFileRunner) Test(file *moduletest.File) { // Build the graph for the file. b := graph.TestGraphBuilder{ - File: file, - GlobalVars: runner.EvalContext.VariableCaches.GlobalVariables, - ContextOpts: runner.Suite.Opts, + File: file, + GlobalVars: runner.EvalContext.VariableCaches.GlobalVariables, + ContextOpts: runner.Suite.Opts, + BackendFactory: runner.Suite.BackendFactory, } g, diags := b.Build() file.Diagnostics = file.Diagnostics.Append(diags) diff --git a/internal/command/test.go b/internal/command/test.go index 1af06d3e48..761230ae31 100644 --- a/internal/command/test.go +++ b/internal/command/test.go @@ -260,7 +260,8 @@ func (c *TestCommand) Run(rawArgs []string) int { } } else { localRunner := &local.TestSuiteRunner{ - Config: config, + BackendFactory: backendInit.Backend, + Config: config, // The GlobalVariables are loaded from the // main configuration directory // The GlobalTestVariables are loaded from the diff --git a/internal/moduletest/graph/test_graph_builder.go b/internal/moduletest/graph/test_graph_builder.go index 43213589a4..49cbfd85d0 100644 --- a/internal/moduletest/graph/test_graph_builder.go +++ b/internal/moduletest/graph/test_graph_builder.go @@ -7,6 +7,7 @@ import ( "log" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/dag" "github.com/hashicorp/terraform/internal/moduletest" @@ -18,9 +19,10 @@ import ( // a terraform test file. The file may contain multiple runs, and each run may have // dependencies on other runs. type TestGraphBuilder struct { - File *moduletest.File - GlobalVars map[string]backendrun.UnparsedVariableValue - ContextOpts *terraform.ContextOpts + File *moduletest.File + GlobalVars map[string]backendrun.UnparsedVariableValue + ContextOpts *terraform.ContextOpts + BackendFactory func(string) backend.InitFn } type graphOptions struct { @@ -47,7 +49,9 @@ func (b *TestGraphBuilder) Steps() []terraform.GraphTransformer { } steps := []terraform.GraphTransformer{ &TestRunTransformer{opts}, - &TestStateTransformer{File: b.File}, + // Could setting initial state based on backends be better-implemented as + // another transformer? + &TestStateTransformer{File: b.File, BackendFactory: b.BackendFactory}, &TestStateCleanupTransformer{opts}, terraform.DynamicTransformer(validateRunConfigs), &TestProvidersTransformer{}, diff --git a/internal/moduletest/graph/transform_state.go b/internal/moduletest/graph/transform_state.go index 96066da9ac..0023bf9642 100644 --- a/internal/moduletest/graph/transform_state.go +++ b/internal/moduletest/graph/transform_state.go @@ -5,9 +5,12 @@ package graph import ( "fmt" + "log" "maps" "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/dag" "github.com/hashicorp/terraform/internal/moduletest" @@ -32,7 +35,8 @@ type TestFileState struct { // TestStateTransformer is a GraphTransformer that initializes the context with // all the states produced by the test file. type TestStateTransformer struct { - File *moduletest.File + File *moduletest.File + BackendFactory func(string) backend.InitFn } func (t *TestStateTransformer) Transform(g *terraform.Graph) error { @@ -48,11 +52,44 @@ func (t *TestStateTransformer) Transform(g *terraform.Graph) error { for node := range dag.SelectSeq(g.VerticesSeq(), runFilter) { key := node.run.Config.StateKey if _, exists := statesMap[key]; !exists { - state := &TestFileState{ - File: t.File, - Run: nil, - State: states.NewState(), + + var state *TestFileState + + if bc, exists := t.File.Config.BackendConfigs[key]; exists { + // If the state for that state key should come from a backend, + // obtain and use that + if t.BackendFactory == nil { + 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) + } + + f := t.BackendFactory(bc.Backend.Type) + if f == nil { + 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) + } + be, err := getBackendInstance(key, bc.Backend, f) + if err != nil { + return err + } + + stmgr, err := be.StateMgr(backend.DefaultStateName) // We only allow use of the default workspace + if err != nil { + return fmt.Errorf("error retrieving state for state key %q from backend: error retrieving state manager: %w", key, err) + } + + 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) + state = &TestFileState{ + Run: nil, + State: stmgr.State(), + } + } else { + // Else, set an empty in-memory state for the state key + log.Printf("[TRACE] TestConfigTransformer.Transform: set initial state for state key %q as empty state", key) + state = &TestFileState{ + Run: nil, + State: states.NewState(), + } } + statesMap[key] = state } @@ -64,7 +101,39 @@ func (t *TestStateTransformer) Transform(g *terraform.Graph) error { return nil } -func (t *TestStateTransformer) addRootConfigNode(g *terraform.Graph, statesMap map[string]*TestFileState) *dynamicNode { +// getBackendInstance uses the config for a given run block's backend block to create and return a configured +// instance of that backend type. +func getBackendInstance(stateKey string, config *configs.Backend, f backend.InitFn) (backend.Backend, error) { + b := f() + log.Printf("[TRACE] TestConfigTransformer.Transform: instantiated backend of type %T", b) + + schema := b.ConfigSchema() + decSpec := schema.NoneRequired().DecoderSpec() + configVal, hclDiags := hcldec.Decode(config.Config, decSpec, nil) + if hclDiags.HasErrors() { + return nil, fmt.Errorf("error decoding backend configuration for state key %s : %v", stateKey, hclDiags.Errs()) + } + + if !configVal.IsWhollyKnown() { + return nil, fmt.Errorf("unknown values within backend definition for state key %s", stateKey) + } + + newVal, validateDiags := b.PrepareConfig(configVal) + validateDiags = validateDiags.InConfigBody(config.Config, "") + if validateDiags.HasErrors() { + return nil, validateDiags.Err() + } + + configureDiags := b.Configure(newVal) + configureDiags = configureDiags.InConfigBody(config.Config, "") + if validateDiags.HasErrors() { + return nil, configureDiags.Err() + } + + return b, nil +} + +func (t *TestConfigTransformer) addRootConfigNode(g *terraform.Graph, statesMap map[string]*TestFileState) *dynamicNode { rootConfigNode := &dynamicNode{ eval: func(ctx *EvalContext) tfdiags.Diagnostics { var diags tfdiags.Diagnostics