mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-05-05 00:00:31 -04:00
Because of the support for provider-contributed functions, expression evaluation can potentially cause provider gRPC requests to happen, and so we'll need to be able to plumb OpenTelemetry trace information through to those calls. This initial commit focuses mainly on just getting the functions in lang.Scope set up to take context.Context, along with their companions in configs.StaticEvaluator, while leaving most of the callers just passing context.TODO() for now so we can gradually deal with the rest of the plumbing in later commits. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
271 lines
8.9 KiB
Go
271 lines
8.9 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package tofu
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"sync"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/convert"
|
|
"github.com/zclconf/go-cty/cty/function"
|
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/configs"
|
|
"github.com/opentofu/opentofu/internal/lang"
|
|
"github.com/opentofu/opentofu/internal/lang/marks"
|
|
"github.com/opentofu/opentofu/internal/moduletest"
|
|
"github.com/opentofu/opentofu/internal/plans"
|
|
"github.com/opentofu/opentofu/internal/providers"
|
|
"github.com/opentofu/opentofu/internal/states"
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
)
|
|
|
|
// TestContext wraps a Context, and adds in direct values for the current state,
|
|
// most recent plan, and configuration.
|
|
//
|
|
// This combination allows functions called on the TestContext to create a
|
|
// complete scope to evaluate test assertions.
|
|
type TestContext struct {
|
|
*Context
|
|
|
|
Config *configs.Config
|
|
State *states.State
|
|
Plan *plans.Plan
|
|
Variables InputValues
|
|
}
|
|
|
|
// TestContext creates a TestContext structure that can evaluate test assertions
|
|
// against the provided state and plan.
|
|
func (c *Context) TestContext(config *configs.Config, state *states.State, plan *plans.Plan, variables InputValues) *TestContext {
|
|
return &TestContext{
|
|
Context: c,
|
|
Config: config,
|
|
State: state,
|
|
Plan: plan,
|
|
Variables: variables,
|
|
}
|
|
}
|
|
|
|
// EvaluateAgainstState processes the assertions inside the provided
|
|
// configs.TestRun against the embedded state.
|
|
//
|
|
// The provided plan is import as it is needed to evaluate the `plantimestamp`
|
|
// function, but no data or changes from the embedded plan is referenced in
|
|
// this function.
|
|
func (ctx *TestContext) EvaluateAgainstState(run *moduletest.Run) {
|
|
defer ctx.acquireRun("evaluate")()
|
|
ctx.evaluate(ctx.State.SyncWrapper(), plans.NewChanges().SyncWrapper(), run, walkApply)
|
|
}
|
|
|
|
// EvaluateAgainstPlan processes the assertions inside the provided
|
|
// configs.TestRun against the embedded plan and state.
|
|
func (ctx *TestContext) EvaluateAgainstPlan(run *moduletest.Run) {
|
|
defer ctx.acquireRun("evaluate")()
|
|
ctx.evaluate(ctx.State.SyncWrapper(), ctx.Plan.Changes.SyncWrapper(), run, walkPlan)
|
|
}
|
|
|
|
func (tc *TestContext) evaluate(state *states.SyncState, changes *plans.ChangesSync, run *moduletest.Run, operation walkOperation) {
|
|
// The state does not include the module that has no resources, making its outputs unusable.
|
|
// synchronizeStates function synchronizes the state with the planned state, ensuring inclusion of all modules.
|
|
if tc.Plan != nil && tc.Plan.PlannedState != nil &&
|
|
len(tc.State.Modules) != len(tc.Plan.PlannedState.Modules) {
|
|
state = synchronizeStates(tc.State, tc.Plan.PlannedState)
|
|
}
|
|
|
|
data := &evaluationStateData{
|
|
Evaluator: &Evaluator{
|
|
Operation: operation,
|
|
Meta: tc.meta,
|
|
Config: tc.Config,
|
|
Plugins: tc.plugins,
|
|
State: state,
|
|
Changes: changes,
|
|
VariableValues: func() map[string]map[string]cty.Value {
|
|
variables := map[string]map[string]cty.Value{
|
|
addrs.RootModule.String(): make(map[string]cty.Value),
|
|
}
|
|
for name, variable := range tc.Variables {
|
|
variables[addrs.RootModule.String()][name] = variable.Value
|
|
}
|
|
return variables
|
|
}(),
|
|
VariableValuesLock: new(sync.Mutex),
|
|
PlanTimestamp: tc.Plan.Timestamp,
|
|
},
|
|
ModulePath: nil, // nil for the root module
|
|
InstanceKeyData: EvalDataForNoInstanceKey,
|
|
Operation: operation,
|
|
}
|
|
|
|
var providerInstanceLock sync.Mutex
|
|
providerInstances := make(map[addrs.Provider]providers.Interface)
|
|
defer func() {
|
|
for addr, inst := range providerInstances {
|
|
log.Printf("[INFO] Shutting down test provider %s", addr)
|
|
inst.Close(context.TODO())
|
|
}
|
|
}()
|
|
|
|
providerSupplier := func(addr addrs.Provider) providers.Interface {
|
|
providerInstanceLock.Lock()
|
|
defer providerInstanceLock.Unlock()
|
|
|
|
if inst, ok := providerInstances[addr]; ok {
|
|
return inst
|
|
}
|
|
|
|
factory, ok := tc.plugins.providerFactories[addr]
|
|
if !ok {
|
|
log.Printf("[WARN] Unable to find provider %s in test context", addr)
|
|
providerInstances[addr] = nil
|
|
return nil
|
|
}
|
|
log.Printf("[INFO] Starting test provider %s", addr)
|
|
inst, err := factory()
|
|
if err != nil {
|
|
log.Printf("[WARN] Unable to start provider %s in test context", addr)
|
|
providerInstances[addr] = nil
|
|
return nil
|
|
} else {
|
|
log.Printf("[INFO] Shutting down test provider %s", addr)
|
|
providerInstances[addr] = inst
|
|
return inst
|
|
}
|
|
}
|
|
|
|
scope := &lang.Scope{
|
|
Data: data,
|
|
BaseDir: ".",
|
|
PureOnly: operation != walkApply,
|
|
PlanTimestamp: tc.Plan.Timestamp,
|
|
ProviderFunctions: func(ctx context.Context, pf addrs.ProviderFunction, rng tfdiags.SourceRange) (*function.Function, tfdiags.Diagnostics) {
|
|
// This is a simpler flow than what is allowed during normal exection.
|
|
// We only support non-configured functions here.
|
|
pr, ok := tc.Config.Module.ProviderRequirements.RequiredProviders[pf.ProviderName]
|
|
if !ok {
|
|
return nil, tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Unknown function provider",
|
|
Detail: fmt.Sprintf("Provider %q does not exist within the required_providers of this module", pf.ProviderName),
|
|
Subject: rng.ToHCL().Ptr(),
|
|
})
|
|
}
|
|
|
|
provider := providerSupplier(pr.Type)
|
|
|
|
return evalContextProviderFunction(ctx, provider, walkPlan, pf, rng)
|
|
},
|
|
}
|
|
|
|
// We're going to assume the run has passed, and then if anything fails this
|
|
// value will be updated.
|
|
run.Status = run.Status.Merge(moduletest.Pass)
|
|
|
|
// Now validate all the assertions within this run block.
|
|
for _, rule := range run.Config.CheckRules {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
refs, moreDiags := lang.ReferencesInExpr(addrs.ParseRefFromTestingScope, rule.Condition)
|
|
diags = diags.Append(moreDiags)
|
|
moreRefs, moreDiags := lang.ReferencesInExpr(addrs.ParseRefFromTestingScope, rule.ErrorMessage)
|
|
diags = diags.Append(moreDiags)
|
|
refs = append(refs, moreRefs...)
|
|
|
|
hclCtx, moreDiags := scope.EvalContext(context.TODO(), refs)
|
|
diags = diags.Append(moreDiags)
|
|
|
|
errorMessage, moreDiags := evalCheckErrorMessage(rule.ErrorMessage, hclCtx)
|
|
diags = diags.Append(moreDiags)
|
|
|
|
runVal, hclDiags := rule.Condition.Value(hclCtx)
|
|
diags = diags.Append(hclDiags)
|
|
|
|
runVal, deprDiags := marks.ExtractDeprecatedDiagnosticsWithExpr(runVal, rule.Condition)
|
|
diags = diags.Append(deprDiags)
|
|
|
|
run.Diagnostics = run.Diagnostics.Append(diags)
|
|
if diags.HasErrors() {
|
|
run.Status = run.Status.Merge(moduletest.Error)
|
|
continue
|
|
}
|
|
|
|
// The condition result may be marked if the expression refers to a
|
|
// sensitive value.
|
|
runVal, _ = runVal.Unmark()
|
|
|
|
if runVal.IsNull() {
|
|
run.Status = run.Status.Merge(moduletest.Error)
|
|
run.Diagnostics = run.Diagnostics.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid condition run",
|
|
Detail: "Condition expression must return either true or false, not null.",
|
|
Subject: rule.Condition.Range().Ptr(),
|
|
Expression: rule.Condition,
|
|
EvalContext: hclCtx,
|
|
})
|
|
continue
|
|
}
|
|
|
|
if !runVal.IsKnown() {
|
|
run.Status = run.Status.Merge(moduletest.Error)
|
|
run.Diagnostics = run.Diagnostics.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Unknown condition run",
|
|
Detail: "Condition expression could not be evaluated at this time.",
|
|
Subject: rule.Condition.Range().Ptr(),
|
|
Expression: rule.Condition,
|
|
EvalContext: hclCtx,
|
|
})
|
|
continue
|
|
}
|
|
|
|
var err error
|
|
if runVal, err = convert.Convert(runVal, cty.Bool); err != nil {
|
|
run.Status = run.Status.Merge(moduletest.Error)
|
|
run.Diagnostics = run.Diagnostics.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid condition run",
|
|
Detail: fmt.Sprintf("Invalid condition run value: %s.", tfdiags.FormatError(err)),
|
|
Subject: rule.Condition.Range().Ptr(),
|
|
Expression: rule.Condition,
|
|
EvalContext: hclCtx,
|
|
})
|
|
continue
|
|
}
|
|
|
|
if runVal.False() {
|
|
run.Status = run.Status.Merge(moduletest.Fail)
|
|
run.Diagnostics = run.Diagnostics.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Test assertion failed",
|
|
Detail: errorMessage,
|
|
Subject: rule.Condition.Range().Ptr(),
|
|
Expression: rule.Condition,
|
|
EvalContext: hclCtx,
|
|
})
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
// synchronizeStates compares the planned state to the current state and incorporates any missing modules
|
|
// from the planned state into the current state.
|
|
//
|
|
// If a module has no resources, it is included in the current state to ensure that its output variables are usable.
|
|
func synchronizeStates(state, plannedState *states.State) *states.SyncState {
|
|
newState := state.DeepCopy()
|
|
for key, value := range plannedState.Modules {
|
|
if _, exists := newState.Modules[key]; !exists {
|
|
newState.Modules[key] = value
|
|
}
|
|
}
|
|
return newState.SyncWrapper()
|
|
}
|