Files
opentf/internal/engine/internal/execgraph/graph.go
Martin Atkins 2a9dfed739 execgraph: Constant DeposedKey values
In order to describe operations against deposed objects for managed
resource instances we need to be able to store constant states.DeposedKey
values in the execution graph.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2026-02-09 07:35:50 -08:00

230 lines
9.4 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 execgraph
import (
"fmt"
"maps"
"slices"
"strconv"
"strings"
"github.com/zclconf/go-cty-debug/ctydebug"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/lang/grapheval"
"github.com/opentofu/opentofu/internal/states"
)
type Graph struct {
// Overall "graph" is modelled as a collection of tables representing
// different kinds of nodes, and then the actual graph relationships are
// modeled as [ResultRef] or [AnyResultRef] values that are all really
// just indices into these tables.
//////// Constants saved directly in the graph
// The tables in this section are values that are decided during the
// planning phase and need not be recalculated during the apply phase,
// and so we just store them directly.
// constantVals is the table of constant values that are to be saved
// directly inside the execution graph.
constantVals []cty.Value
// resourceInstAddrs is the table of resource instance addresses that are to
// be saved directly inside the execution graph.
resourceInstAddrs []addrs.AbsResourceInstance
// deposedKeys is the table of deposed keys that are to be saved directly
// inside the execution graph.
deposedKeys []states.DeposedKey
// providerInstAddrs is the table of provider instance addresses that are to
// be saved directly inside the execution graph.
providerInstAddrs []addrs.AbsProviderInstanceCorrect
//////// The actual side-effects
// The tables in this section deal with the main side-effects that we're
// intending to perform, and modelling the interactions between them.
//
// These are the only graph nodes that can directly depend on results from
// other graph nodes. Everything in the other sections above is fetching
// data from outside of the apply engine, although those which interact
// with the ApplyOracle will often depend indirectly on results in this
// section where the configuration defines the desired state for one
// resource instance in terms of the final state of another resource
// instance.
// ops are the actual operations -- functions with side-effects --
// that are the main purpose of the execution graph. Operations can
// depend on each other and on constant values or state references.
ops []operationDesc
// waiters are nodes that just express a dependency on the work that
// produces some other results even though the actual value of the
// result isn't needed. For example, this can be used to describe
// what other work needs to complete before a provider instance is closed.
//
// Although it's not actually enforced by the model, it's only really useful
// to add _operation_ results to "waiter" nodes, because operations are
// how we model side-effects that we might need to wait for completion of.
waiters [][]AnyResultRef
// resourceInstanceResults are "sink" nodes that capture references to
// the "final state" results for desired resource instances that are
// subject to changes in this graph, allowing the resulting values to
// propagate back into the evaluation system so that downstream resource
// instance configurations can be derived from them.
//
// Due to the behavior of the concurrently-running expression evaluation
// system, there's an effective implied dependency edge between results
// captured in here and the entries in desiredStateRefs for any resource
// instances whose configuration is derived from the result of an entry
// in this map. However, the execution graph is not supposed to rely on
// those implied edges for correct execution order: the "final plan"
// operation for each resource instance should also directly depend on
// the results of any resource instances that were identified as
// resource-instance-graph dependencies during the planning process.
resourceInstanceResults addrs.Map[addrs.AbsResourceInstance, ResourceInstanceResultRef]
}
// PromiseDrivenRequestInfo is part of our somewhat-roundabout means of
// gathering additional context about promise-based requests only when we
// enounter an error that makes it worth gathering this information.
//
// Given a [PromiseDrivenResultKey] previously advertised to a
// [RequestTrackerWithNotify] implementation used with [CompiledGraph.Execute]
// on a compiled version of the same graph, this returns a
// [grapheval.RequestInfo] value summarizing what that request was intending
// to achieve in terms that are suitable to include in an error message that
// someone will presmuably submit in an OpenTofu bug report, because
// promise-related errors should only crop up during processing of
// incorrectly-constructed execution graphs.
func (g *Graph) PromiseDrivenRequestInfo(key PromiseDrivenResultKey) grapheval.RequestInfo {
opIdx := key.operationIdx
if opIdx < 0 || opIdx >= len(g.ops) {
// We seem to have a key produced from a different graph than this one,
// but we'll tolerate this and just return a placeholder so that we
// can hopefully return at least a partial error message.
return grapheval.RequestInfo{
Name: "unknown execution graph operation (this is a bug in OpenTofu)",
}
}
opDesc := g.ops[opIdx]
return grapheval.RequestInfo{
Name: "internal operation: " + g.operationDebugSummary(opDesc),
}
}
func (g *Graph) operationDebugSummary(opDesc operationDesc) string {
var buf strings.Builder
buf.WriteString(strings.TrimPrefix(opDesc.opCode.String(), "op"))
buf.WriteByte('(')
for argIdx, arg := range opDesc.operands {
if argIdx != 0 {
buf.WriteString(", ")
}
switch arg := arg.(type) {
case anyOperationResultRef:
// We'll show this as a nested function call so that we can also
// see whatever arguments it has, since hopefully this will
// eventually lead to something useful like a resource instance addr.
childOpIdx := arg.operationResultIndex()
opDesc := g.ops[childOpIdx]
buf.WriteString(g.operationDebugSummary(opDesc))
case valueResultRef:
v := g.constantVals[arg.index]
fmt.Fprintf(&buf, "<%s value>", v.Type().FriendlyName())
case resourceInstAddrResultRef, providerInstAddrResultRef, waiterResultRef, nil:
// Our resultDebugRepr representation is fine for these ones
buf.WriteString(g.resultDebugRepr(arg))
default:
// We don't print out anything we're not already familiar with
// because we might add a new kind of result later that isn't
// appropriate to include in a UI-facing error message.
buf.WriteString("[...]")
}
}
buf.WriteByte(')')
return buf.String()
}
// DebugRepr returns a relatively-concise string representation of the
// graph which includes all of the registered operations and their operands,
// along with any constant values they rely on.
//
// The result is intended primarily for human consumption when testing or
// debugging. It's not an executable or parseable representation and details
// about how it's formatted might change over time.
func (g *Graph) DebugRepr() string {
var buf strings.Builder
for idx, val := range g.constantVals {
fmt.Fprintf(&buf, "v[%d] = %s;\n", idx, strings.TrimSpace(ctydebug.ValueString(val)))
}
if len(g.constantVals) != 0 && (len(g.ops) != 0 || g.resourceInstanceResults.Len() != 0) {
buf.WriteByte('\n')
}
for idx, op := range g.ops {
fmt.Fprintf(&buf, "r[%d] = %s(", idx, strings.TrimLeft(op.opCode.String(), "op"))
for opIdx, result := range op.operands {
if opIdx != 0 {
buf.WriteString(", ")
}
buf.WriteString(g.resultDebugRepr(result))
}
buf.WriteString(");\n")
}
if g.resourceInstanceResults.Len() != 0 && (len(g.ops) != 0 || len(g.constantVals) != 0) {
buf.WriteByte('\n')
}
// We'll sort the resource instance results by instance address key just
// so that the resulting order is consistent for comparison in tests.
resourceInstanceResults := make(map[string]string)
for _, elem := range g.resourceInstanceResults.Elems {
resourceInstanceResults[elem.Key.String()] = g.resultDebugRepr(elem.Value)
}
resourceInstanceAddrs := slices.Collect(maps.Keys(resourceInstanceResults))
slices.Sort(resourceInstanceAddrs)
for _, addrStr := range resourceInstanceAddrs {
fmt.Fprintf(&buf, "%s = %s;\n", addrStr, resourceInstanceResults[addrStr])
}
return buf.String()
}
func (g *Graph) resultDebugRepr(result AnyResultRef) string {
switch result := result.(type) {
case valueResultRef:
return fmt.Sprintf("v[%d]", result.index)
case resourceInstAddrResultRef:
resourceInstAddr := g.resourceInstAddrs[result.index]
return resourceInstAddr.String()
case deposedKeyResultRef:
deposedKey := g.deposedKeys[result.index]
return strconv.Quote(deposedKey.String())
case providerInstAddrResultRef:
providerInstAddr := g.providerInstAddrs[result.index]
return providerInstAddr.String()
case anyOperationResultRef:
return fmt.Sprintf("r[%d]", result.operationResultIndex())
case waiterResultRef:
awaiting := g.waiters[result.index]
var buf strings.Builder
buf.WriteString("await(")
for i, r := range awaiting {
if i != 0 {
buf.WriteString(", ")
}
buf.WriteString(g.resultDebugRepr(r))
}
buf.WriteString(")")
return buf.String()
case nil:
return "nil"
default:
// Should try to keep the above cases comprehensive because
// this default is not very readable and might even be
// useless if it's a reference into a table we're not otherwise
// including the output here.
return fmt.Sprintf("%#v", result)
}
}