github.com/opentofu/opentofu@v1.7.1/internal/tofu/test_context.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package tofu
     7  
     8  import (
     9  	"fmt"
    10  	"log"
    11  	"sync"
    12  
    13  	"github.com/hashicorp/hcl/v2"
    14  	"github.com/zclconf/go-cty/cty"
    15  	"github.com/zclconf/go-cty/cty/convert"
    16  	"github.com/zclconf/go-cty/cty/function"
    17  
    18  	"github.com/opentofu/opentofu/internal/addrs"
    19  	"github.com/opentofu/opentofu/internal/configs"
    20  	"github.com/opentofu/opentofu/internal/lang"
    21  	"github.com/opentofu/opentofu/internal/moduletest"
    22  	"github.com/opentofu/opentofu/internal/plans"
    23  	"github.com/opentofu/opentofu/internal/providers"
    24  	"github.com/opentofu/opentofu/internal/states"
    25  	"github.com/opentofu/opentofu/internal/tfdiags"
    26  )
    27  
    28  // TestContext wraps a Context, and adds in direct values for the current state,
    29  // most recent plan, and configuration.
    30  //
    31  // This combination allows functions called on the TestContext to create a
    32  // complete scope to evaluate test assertions.
    33  type TestContext struct {
    34  	*Context
    35  
    36  	Config    *configs.Config
    37  	State     *states.State
    38  	Plan      *plans.Plan
    39  	Variables InputValues
    40  }
    41  
    42  // TestContext creates a TestContext structure that can evaluate test assertions
    43  // against the provided state and plan.
    44  func (c *Context) TestContext(config *configs.Config, state *states.State, plan *plans.Plan, variables InputValues) *TestContext {
    45  	return &TestContext{
    46  		Context:   c,
    47  		Config:    config,
    48  		State:     state,
    49  		Plan:      plan,
    50  		Variables: variables,
    51  	}
    52  }
    53  
    54  // EvaluateAgainstState processes the assertions inside the provided
    55  // configs.TestRun against the embedded state.
    56  //
    57  // The provided plan is import as it is needed to evaluate the `plantimestamp`
    58  // function, but no data or changes from the embedded plan is referenced in
    59  // this function.
    60  func (ctx *TestContext) EvaluateAgainstState(run *moduletest.Run) {
    61  	defer ctx.acquireRun("evaluate")()
    62  	ctx.evaluate(ctx.State.SyncWrapper(), plans.NewChanges().SyncWrapper(), run, walkApply)
    63  }
    64  
    65  // EvaluateAgainstPlan processes the assertions inside the provided
    66  // configs.TestRun against the embedded plan and state.
    67  func (ctx *TestContext) EvaluateAgainstPlan(run *moduletest.Run) {
    68  	defer ctx.acquireRun("evaluate")()
    69  	ctx.evaluate(ctx.State.SyncWrapper(), ctx.Plan.Changes.SyncWrapper(), run, walkPlan)
    70  }
    71  
    72  func (ctx *TestContext) evaluate(state *states.SyncState, changes *plans.ChangesSync, run *moduletest.Run, operation walkOperation) {
    73  	// The state does not include the module that has no resources, making its outputs unusable.
    74  	// synchronizeStates function synchronizes the state with the planned state, ensuring inclusion of all modules.
    75  	if ctx.Plan != nil && ctx.Plan.PlannedState != nil &&
    76  		len(ctx.State.Modules) != len(ctx.Plan.PlannedState.Modules) {
    77  		state = synchronizeStates(ctx.State, ctx.Plan.PlannedState)
    78  	}
    79  
    80  	data := &evaluationStateData{
    81  		Evaluator: &Evaluator{
    82  			Operation: operation,
    83  			Meta:      ctx.meta,
    84  			Config:    ctx.Config,
    85  			Plugins:   ctx.plugins,
    86  			State:     state,
    87  			Changes:   changes,
    88  			VariableValues: func() map[string]map[string]cty.Value {
    89  				variables := map[string]map[string]cty.Value{
    90  					addrs.RootModule.String(): make(map[string]cty.Value),
    91  				}
    92  				for name, variable := range ctx.Variables {
    93  					variables[addrs.RootModule.String()][name] = variable.Value
    94  				}
    95  				return variables
    96  			}(),
    97  			VariableValuesLock: new(sync.Mutex),
    98  			PlanTimestamp:      ctx.Plan.Timestamp,
    99  		},
   100  		ModulePath:      nil, // nil for the root module
   101  		InstanceKeyData: EvalDataForNoInstanceKey,
   102  		Operation:       operation,
   103  	}
   104  
   105  	var providerInstanceLock sync.Mutex
   106  	providerInstances := make(map[addrs.Provider]providers.Interface)
   107  	defer func() {
   108  		for addr, inst := range providerInstances {
   109  			log.Printf("[INFO] Shutting down test provider %s", addr)
   110  			inst.Close()
   111  		}
   112  	}()
   113  
   114  	providerSupplier := func(addr addrs.AbsProviderConfig) providers.Interface {
   115  		providerInstanceLock.Lock()
   116  		defer providerInstanceLock.Unlock()
   117  
   118  		if inst, ok := providerInstances[addr.Provider]; ok {
   119  			return inst
   120  		}
   121  
   122  		factory, ok := ctx.plugins.providerFactories[addr.Provider]
   123  		if !ok {
   124  			log.Printf("[WARN] Unable to find provider %s in test context", addr)
   125  			providerInstances[addr.Provider] = nil
   126  			return nil
   127  		}
   128  		log.Printf("[INFO] Starting test provider %s", addr)
   129  		inst, err := factory()
   130  		if err != nil {
   131  			log.Printf("[WARN] Unable to start provider %s in test context", addr)
   132  			providerInstances[addr.Provider] = nil
   133  			return nil
   134  		} else {
   135  			log.Printf("[INFO] Shutting down test provider %s", addr)
   136  			providerInstances[addr.Provider] = inst
   137  			return inst
   138  		}
   139  	}
   140  
   141  	scope := &lang.Scope{
   142  		Data:          data,
   143  		BaseDir:       ".",
   144  		PureOnly:      operation != walkApply,
   145  		PlanTimestamp: ctx.Plan.Timestamp,
   146  		ProviderFunctions: func(pf addrs.ProviderFunction, rng tfdiags.SourceRange) (*function.Function, tfdiags.Diagnostics) {
   147  			return evalContextProviderFunction(providerSupplier, ctx.Config, walkPlan, pf, rng)
   148  		},
   149  	}
   150  
   151  	// We're going to assume the run has passed, and then if anything fails this
   152  	// value will be updated.
   153  	run.Status = run.Status.Merge(moduletest.Pass)
   154  
   155  	// Now validate all the assertions within this run block.
   156  	for _, rule := range run.Config.CheckRules {
   157  		var diags tfdiags.Diagnostics
   158  
   159  		refs, moreDiags := lang.ReferencesInExpr(addrs.ParseRefFromTestingScope, rule.Condition)
   160  		diags = diags.Append(moreDiags)
   161  		moreRefs, moreDiags := lang.ReferencesInExpr(addrs.ParseRefFromTestingScope, rule.ErrorMessage)
   162  		diags = diags.Append(moreDiags)
   163  		refs = append(refs, moreRefs...)
   164  
   165  		hclCtx, moreDiags := scope.EvalContext(refs)
   166  		diags = diags.Append(moreDiags)
   167  
   168  		errorMessage, moreDiags := evalCheckErrorMessage(rule.ErrorMessage, hclCtx)
   169  		diags = diags.Append(moreDiags)
   170  
   171  		runVal, hclDiags := rule.Condition.Value(hclCtx)
   172  		diags = diags.Append(hclDiags)
   173  
   174  		run.Diagnostics = run.Diagnostics.Append(diags)
   175  		if diags.HasErrors() {
   176  			run.Status = run.Status.Merge(moduletest.Error)
   177  			continue
   178  		}
   179  
   180  		// The condition result may be marked if the expression refers to a
   181  		// sensitive value.
   182  		runVal, _ = runVal.Unmark()
   183  
   184  		if runVal.IsNull() {
   185  			run.Status = run.Status.Merge(moduletest.Error)
   186  			run.Diagnostics = run.Diagnostics.Append(&hcl.Diagnostic{
   187  				Severity:    hcl.DiagError,
   188  				Summary:     "Invalid condition run",
   189  				Detail:      "Condition expression must return either true or false, not null.",
   190  				Subject:     rule.Condition.Range().Ptr(),
   191  				Expression:  rule.Condition,
   192  				EvalContext: hclCtx,
   193  			})
   194  			continue
   195  		}
   196  
   197  		if !runVal.IsKnown() {
   198  			run.Status = run.Status.Merge(moduletest.Error)
   199  			run.Diagnostics = run.Diagnostics.Append(&hcl.Diagnostic{
   200  				Severity:    hcl.DiagError,
   201  				Summary:     "Unknown condition run",
   202  				Detail:      "Condition expression could not be evaluated at this time.",
   203  				Subject:     rule.Condition.Range().Ptr(),
   204  				Expression:  rule.Condition,
   205  				EvalContext: hclCtx,
   206  			})
   207  			continue
   208  		}
   209  
   210  		var err error
   211  		if runVal, err = convert.Convert(runVal, cty.Bool); err != nil {
   212  			run.Status = run.Status.Merge(moduletest.Error)
   213  			run.Diagnostics = run.Diagnostics.Append(&hcl.Diagnostic{
   214  				Severity:    hcl.DiagError,
   215  				Summary:     "Invalid condition run",
   216  				Detail:      fmt.Sprintf("Invalid condition run value: %s.", tfdiags.FormatError(err)),
   217  				Subject:     rule.Condition.Range().Ptr(),
   218  				Expression:  rule.Condition,
   219  				EvalContext: hclCtx,
   220  			})
   221  			continue
   222  		}
   223  
   224  		if runVal.False() {
   225  			run.Status = run.Status.Merge(moduletest.Fail)
   226  			run.Diagnostics = run.Diagnostics.Append(&hcl.Diagnostic{
   227  				Severity:    hcl.DiagError,
   228  				Summary:     "Test assertion failed",
   229  				Detail:      errorMessage,
   230  				Subject:     rule.Condition.Range().Ptr(),
   231  				Expression:  rule.Condition,
   232  				EvalContext: hclCtx,
   233  			})
   234  			continue
   235  		}
   236  	}
   237  }
   238  
   239  // synchronizeStates compares the planned state to the current state and incorporates any missing modules
   240  // from the planned state into the current state.
   241  //
   242  // If a module has no resources, it is included in the current state to ensure that its output variables are usable.
   243  func synchronizeStates(state, plannedState *states.State) *states.SyncState {
   244  	newState := state.DeepCopy()
   245  	for key, value := range plannedState.Modules {
   246  		if _, exists := newState.Modules[key]; !exists {
   247  			newState.Modules[key] = value
   248  		}
   249  	}
   250  	return newState.SyncWrapper()
   251  }