mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-04-07 21:01:46 -04:00
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>
230 lines
9.4 KiB
Go
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)
|
|
}
|
|
}
|