blob: 317c67045290b64ec07902d5017c803e54f6994f [file] [log] [blame] [edit]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package graph
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/dag"
"github.com/hashicorp/terraform/internal/moduletest"
hcltest "github.com/hashicorp/terraform/internal/moduletest/hcl"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
)
type GraphNodeExecutable interface {
Execute(ctx *EvalContext) tfdiags.Diagnostics
}
// TestFileState is a helper struct that just maps a run block to the state that
// was produced by the execution of that run block.
type TestFileState struct {
Run *moduletest.Run
State *states.State
}
// TestConfigTransformer is a GraphTransformer that adds all the test runs,
// and the variables defined in each run block, to the graph.
type TestConfigTransformer struct {
File *moduletest.File
}
func (t *TestConfigTransformer) Transform(g *terraform.Graph) error {
// This map tracks the state of each run in the file. If multiple runs
// have the same state key, they will share the same state.
statesMap := make(map[string]*TestFileState)
// a root config node that will add the file states to the context
rootConfigNode := t.addRootConfigNode(g, statesMap)
for _, v := range g.Vertices() {
node, ok := v.(*NodeTestRun)
if !ok {
continue
}
key := node.run.GetStateKey()
if _, exists := statesMap[key]; !exists {
state := &TestFileState{
Run: nil,
State: states.NewState(),
}
statesMap[key] = state
}
// Connect all the test runs to the config node, so that the config node
// is executed before any of the test runs.
g.Connect(dag.BasicEdge(node, rootConfigNode))
}
return 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
ctx.FileStates = statesMap
return diags
},
}
g.Add(rootConfigNode)
return rootConfigNode
}
// TransformConfigForRun transforms the run's module configuration to include
// the providers and variables from its block and the test file.
//
// In practice, this actually just means performing some surgery on the
// available providers. We want to copy the relevant providers from the test
// file into the configuration. We also want to process the providers so they
// use variables from the file instead of variables from within the test file.
func TransformConfigForRun(ctx *EvalContext, run *moduletest.Run, file *moduletest.File) hcl.Diagnostics {
var diags hcl.Diagnostics
// Currently, we only need to override the provider settings.
//
// We can have a set of providers defined within the config, we can also
// have a set of providers defined within the test file. Then the run can
// also specify a set of overrides that tell Terraform exactly which
// providers from the test file to apply into the config.
//
// The process here is as follows:
// 1. Take all the providers in the original config keyed by name.alias,
// we call this `previous`
// 2. Copy them all into a new map, we call this `next`.
// 3a. If the run has configuration specifying provider overrides, we copy
// only the specified providers from the test file into `next`. While
// doing this we ensure to preserve the name and alias from the
// original config.
// 3b. If the run has no override configuration, we copy all the providers
// from the test file into `next`, overriding all providers with name
// collisions from the original config.
// 4. We then modify the original configuration so that the providers it
// holds are the combination specified by the original config, the test
// file and the run file.
// 5. We then return a function that resets the original config back to
// its original state. This can be called by the surrounding test once
// completed so future run blocks can safely execute.
// First, initialise `previous` and `next`. `previous` contains a backup of
// the providers from the original config. `next` contains the set of
// providers that will be used by the test. `next` starts with the set of
// providers from the original config.
previous := run.ModuleConfig.Module.ProviderConfigs
next := make(map[string]*configs.Provider)
for key, value := range previous {
next[key] = value
}
runOutputs := ctx.GetOutputs()
if len(run.Config.Providers) > 0 {
// Then we'll only copy over and overwrite the specific providers asked
// for by this run block.
for _, ref := range run.Config.Providers {
testProvider, ok := file.Config.Providers[ref.InParent.String()]
if !ok {
// Then this reference was invalid as we didn't have the
// specified provider in the parent. This should have been
// caught earlier in validation anyway so is unlikely to happen.
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Missing provider definition for %s", ref.InParent.String()),
Detail: "This provider block references a provider definition that does not exist.",
Subject: ref.InParent.NameRange.Ptr(),
})
continue
}
next[ref.InChild.String()] = &configs.Provider{
Name: ref.InChild.Name,
NameRange: ref.InChild.NameRange,
Alias: ref.InChild.Alias,
AliasRange: ref.InChild.AliasRange,
Config: &hcltest.ProviderConfig{
Original: testProvider.Config,
VariableCache: ctx.GetCache(run),
AvailableRunOutputs: runOutputs,
},
Mock: testProvider.Mock,
MockData: testProvider.MockData,
DeclRange: testProvider.DeclRange,
}
}
} else {
// Otherwise, let's copy over and overwrite all providers specified by
// the test file itself.
for key, provider := range file.Config.Providers {
if !ctx.ProviderExists(run, key) {
// Then we don't actually need this provider for this
// configuration, so skip it.
continue
}
next[key] = &configs.Provider{
Name: provider.Name,
NameRange: provider.NameRange,
Alias: provider.Alias,
AliasRange: provider.AliasRange,
Config: &hcltest.ProviderConfig{
Original: provider.Config,
VariableCache: ctx.GetCache(run),
AvailableRunOutputs: runOutputs,
},
Mock: provider.Mock,
MockData: provider.MockData,
DeclRange: provider.DeclRange,
}
}
}
run.ModuleConfig.Module.ProviderConfigs = next
return diags
}