Files
opentf/internal/tofu/transform_check.go
Martin Atkins 75c7834ca6 tofu: GraphTransformer.Transform takes context.Context
Most of our transformers are pure compute and so don't really have a strong
need to generate trace spans under our current focus of only exposing
user-facing concepts and external requests in our traces, but unfortunately
some of them indirectly depend on provider schema, which in turn means that
they can potentially be unlucky enough to be the trigger for making all
of the provider requests needed to fill the schema cache and therefore
would end up with provider request spans being reported beneath them.

As usual with these interface updates, this initial change focuses only
on changing the interface and updating its direct callers and implementers
to match, without any further refactoring or attempts to plumb contexts
to or from other functions that don't have them yet. That means there are
a few new context.TODO() calls here that we'll tidy up in a later commit
that hopefully won't involve all of the noise that is caused by changing
an interface API.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-05-08 07:16:09 -07:00

149 lines
4.5 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"
"log"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/dag"
)
type checkTransformer struct {
// Config for the entire module.
Config *configs.Config
// Operation is the current operation this node will be part of.
Operation walkOperation
}
var _ GraphTransformer = (*checkTransformer)(nil)
func (t *checkTransformer) Transform(_ context.Context, graph *Graph) error {
return t.transform(graph, t.Config, graph.Vertices())
}
func (t *checkTransformer) transform(g *Graph, cfg *configs.Config, allNodes []dag.Vertex) error {
if t.Operation == walkDestroy || t.Operation == walkPlanDestroy {
// Don't include anything about checks during destroy operations.
//
// For other plan and normal apply operations we do everything, for
// destroy operations we do nothing. For any other operations we still
// include the check nodes, but we don't actually execute the checks
// instead we still validate their references and make sure their
// conditions make sense etc.
return nil
}
moduleAddr := cfg.Path
for _, check := range cfg.Module.Checks {
configAddr := check.Addr().InModule(moduleAddr)
// We want to create a node for each check block. This node will execute
// after anything it references, and will update the checks object
// embedded in the plan and/or state.
log.Printf("[TRACE] checkTransformer: Nodes and edges for %s", configAddr)
expand := &nodeExpandCheck{
addr: configAddr,
config: check,
makeInstance: func(addr addrs.AbsCheck, cfg *configs.Check) dag.Vertex {
return &nodeCheckAssert{
addr: addr,
config: cfg,
executeChecks: t.ExecuteChecks(),
}
},
}
g.Add(expand)
// We also need to report the checks we are going to execute before we
// try and execute them.
if t.ReportChecks() {
report := &nodeReportCheck{
addr: configAddr,
}
g.Add(report)
// Make sure we report our checks before we start executing the
// actual checks.
g.Connect(dag.BasicEdge(expand, report))
if check.DataResource != nil {
// If we have a nested data source, we need to make sure we
// also report the check before the data source executes.
//
// We loop through all the nodes in the graph to find the one
// that contains our data source and connect it.
for _, other := range allNodes {
if resource, isResource := other.(GraphNodeConfigResource); isResource {
resourceAddr := resource.ResourceAddr()
if !resourceAddr.Module.Equal(moduleAddr) {
// This resource isn't in the same module as our check
// so skip it.
continue
}
resourceCfg := cfg.Module.ResourceByAddr(resourceAddr.Resource)
if resourceCfg != nil && resourceCfg.Container != nil && resourceCfg.Container.Accessible(check.Addr()) {
// Make sure we report our checks before we execute any
// embedded data resource.
g.Connect(dag.BasicEdge(other, report))
// There's at most one embedded data source, and
// we've found it so stop looking.
break
}
}
}
}
}
}
for _, child := range cfg.Children {
if err := t.transform(g, child, allNodes); err != nil {
return err
}
}
return nil
}
// ReportChecks returns true if this operation should report any check blocks
// that it is about to execute.
//
// This is true for planning operations, as apply operations recreate the
// expected checks from the plan.
//
// We'll also report the checks during an import operation. We still execute
// our check blocks during an import operation so they need to be reported
// first.
func (t *checkTransformer) ReportChecks() bool {
return t.Operation == walkPlan || t.Operation == walkImport
}
// ExecuteChecks returns true if this operation should actually execute any
// check blocks in the config.
//
// If this returns false we will still create and execute check nodes in the
// graph, but they will only validate things like references and syntax.
func (t *checkTransformer) ExecuteChecks() bool {
switch t.Operation {
case walkPlan, walkApply, walkImport:
// We only actually execute the checks for plan and apply operations.
return true
default:
// For everything else, we still want to validate the checks make sense
// logically and syntactically, but we won't actually resolve the check
// conditions.
return false
}
}