| // 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 |
| } |