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 }