mirror of
https://github.com/hashicorp/terraform.git
synced 2026-03-21 18:10:30 -04:00
Allow use of backend block to set initial state for a state key
This commit is contained in:
parent
a8c57d1d6f
commit
4afc3d79e3
4 changed files with 99 additions and 14 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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{},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue