Files
opentf/internal/tofu/node_resource_apply.go
2026-03-27 20:00:13 +02:00

277 lines
11 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"
"sync"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/dag"
"github.com/opentofu/opentofu/internal/tfdiags"
)
// nodeExpandApplyableResource handles the first layer of resource
// expansion during apply. Even though the resource instances themselves are already expanded from the plan,
// we still need to register resource nodes in instances.Expander with their absolute addresses
// based on the expanded module instances.
//
// "Expand" in the name refers to instances.Expander usage. This node doesn't generate a dynamic subgraph.
type nodeExpandApplyableResource struct {
*NodeAbstractResource
// This slice is meant to keep references to the resourceCloser's of the expanded instances.
// Later, this will be called from nodeCloseableResource.
// At the time of introducing this, it was strictly meant for ephemeral resources, but if there
// will be other closeable resources, this could be used for those too.
closers []resourceCloser
}
var (
_ GraphNodeExecutable = (*nodeExpandApplyableResource)(nil)
_ GraphNodeDynamicExpandable = (*nodeExpandApplyableResource)(nil)
_ GraphNodeReferenceable = (*nodeExpandApplyableResource)(nil)
_ GraphNodeReferencer = (*nodeExpandApplyableResource)(nil)
_ GraphNodeConfigResource = (*nodeExpandApplyableResource)(nil)
_ GraphNodeAttachResourceConfig = (*nodeExpandApplyableResource)(nil)
// nodeExpandApplyableResource needs to be retained during unused nodes pruning
// to register the resource for expanded module instances in `instances.Expander`
_ graphNodeRetainedByPruneUnusedNodesTransformer = (*nodeExpandApplyableResource)(nil)
_ GraphNodeTargetable = (*nodeExpandApplyableResource)(nil)
_ closableResource = (*nodeExpandApplyableResource)(nil)
)
func (n *nodeExpandApplyableResource) retainDuringUnusedPruning() {
}
func (n *nodeExpandApplyableResource) References() []*addrs.Reference {
refs := n.NodeAbstractResource.References()
// The expand node needs to connect to the individual resource instances it
// references, but cannot refer to it's own instances without causing
// cycles. It would be preferable to entirely disallow self references
// without the `self` identifier, but those were allowed in provisioners
// for compatibility with legacy configuration. We also can't always just
// filter them out for all resource node types, because the only method we
// have for catching certain invalid configurations are the cycles that
// result from these inter-instance references.
return filterSelfRefs(n.Addr.Resource, refs)
}
func (n *nodeExpandApplyableResource) Name() string {
return n.NodeAbstractResource.Name() + " (expand)"
}
func (n *nodeExpandApplyableResource) Execute(ctx context.Context, evalCtx EvalContext, op walkOperation) tfdiags.Diagnostics {
// Because ephemeral resources are not stored in plan or state, the apply expansion needs
// to produce a new graph too.
// Therefore, for ephemeral resources, we want to skip the logic from this method since
// the same thing will be executed during nodeExpandApplyableResource.DynamicExpand.
if n.Addr.Resource.Mode == addrs.EphemeralResourceMode {
return nil
}
var diags tfdiags.Diagnostics
expander := evalCtx.InstanceExpander()
moduleInstances := expander.ExpandModule(n.Addr.Module)
for _, module := range moduleInstances {
evalCtx = evalCtx.WithPath(module)
diags = diags.Append(n.writeResourceState(ctx, evalCtx, n.Addr.Resource.Absolute(module)))
}
return diags
}
func (n *nodeExpandApplyableResource) DynamicExpand(evalCtx EvalContext) (*Graph, error) {
// All of the other resource types have their information stored in the plan, but not ephemeral.
// Resource types with information in the plan have the associated instance nodes created during a
// separate transformer (ie: DiffTransfomer).
// For ephemeral resources, we need to create the instance nodes again during the apply graph
// building.
if n.Addr.Resource.Mode != addrs.EphemeralResourceMode {
return nil, nil
}
g, diags := n.expandEphemeralResource(context.TODO(), evalCtx)
return g, diags.ErrWithWarnings()
}
// expandEphemeralResource logic is mostly identical with the nodeExpandPlannableResource.DynamicExpand one.
func (n *nodeExpandApplyableResource) expandEphemeralResource(ctx context.Context, evalCtx EvalContext) (*Graph, tfdiags.Diagnostics) {
var (
diags tfdiags.Diagnostics
g Graph
)
expander := evalCtx.InstanceExpander()
moduleInstances := expander.ExpandModule(n.Addr.Module)
// The above dealt with the expansion of the containing module, so now
// we need to deal with the expansion of the resource itself across all
// instances of the module.
//
// We'll gather up all of the leaf instances we learn about along the way
// so that we can inform the checks subsystem of which instances it should
// be expecting check results for, below.
instAddrs := addrs.MakeSet[addrs.Checkable]()
for _, module := range moduleInstances {
resAddr := n.Addr.Resource.Absolute(module)
expDiags := n.expandEphemeralInstances(ctx, evalCtx, resAddr, &g, instAddrs)
diags = diags.Append(expDiags)
}
if diags.HasErrors() {
return nil, diags
}
// If this is a resource that participates in custom condition checks
// (i.e. it has preconditions or postconditions) then the check state
// wants to know the addresses of the checkable objects so that it can
// treat them as unknown status if we encounter an error before actually
// visiting the checks.
if checkState := evalCtx.Checks(); checkState.ConfigHasChecks(n.NodeAbstractResource.Addr) {
checkState.ReportCheckableObjects(n.NodeAbstractResource.Addr, instAddrs)
}
addRootNodeToGraph(&g)
return &g, diags
}
// expandEphemeralInstances calculates the dynamic expansion for the ephemeral resource
// itself in the context of a particular module instance.
//
// It has several side-effects:
// - Adds a node to Graph g for each leaf resource instance it discovers.
// - Registers the expansion of the resource in the "expander" object embedded inside EvalContext ctx.
// - Adds each present (non-orphaned) resource instance address to instAddrs (guaranteed to always be addrs.AbsResourceInstance, despite being declared as addrs.Checkable).
//
// After calling this for each of the module instances the resource appears
// within, the caller must register the final superset instAddrs with the
// checks subsystem so that it knows the fully expanded set of checkable
// object instances for this resource instance.
func (n *nodeExpandApplyableResource) expandEphemeralInstances(ctx context.Context, globalCtx EvalContext, resAddr addrs.AbsResource, g *Graph, instAddrs addrs.Set[addrs.Checkable]) (diags tfdiags.Diagnostics) {
// The rest of our work here needs to know which module instance it's
// working in, so that it can evaluate expressions in the appropriate scope.
moduleCtx := globalCtx.WithPath(resAddr.Module)
// writeResourceState is responsible for informing the expander of what
// repetition mode this resource has, which allows expander.ExpandResource
// to work below.
moreDiags := n.writeResourceState(ctx, moduleCtx, resAddr)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return diags
}
expander := moduleCtx.InstanceExpander()
instanceAddrs := expander.ExpandResource(resAddr)
for _, addr := range instanceAddrs {
// If this resource is participating in the "checks" mechanism then our
// caller will need to know all of our expanded instance addresses as
// checkable object instances.
// (NOTE: instAddrs probably already has other instance addresses in it
// from earlier calls to this function with different resource addresses,
// because its purpose is to aggregate them all together into a single set.)
instAddrs.Add(addr)
}
// Our graph builder mechanism expects to always be constructing new
// graphs rather than adding to existing ones, so we'll first
// construct a subgraph just for this individual modules's instances and
// then we'll steal all of its nodes and edges to incorporate into our
// main graph which contains all of the resource instances together.
instG, err := n.ephemeralInstanceSubgraph(ctx, resAddr, instanceAddrs)
if err != nil {
diags = diags.Append(err)
return diags
}
g.Subsume(&instG.AcyclicGraph.Graph)
return diags
}
// ephemeralInstanceSubgraph creates the subgraph for the given ephemeral resource in the context of
// a module.
// This also records the closableResource.Close method into nodeExpandApplyableResource.closers to be later used
// to close the ephemeral resources.
func (n *nodeExpandApplyableResource) ephemeralInstanceSubgraph(ctx context.Context, addr addrs.AbsResource, instanceAddrs []addrs.AbsResourceInstance) (*Graph, error) {
var diags tfdiags.Diagnostics
// The concrete resource factory we'll use
concreteResource := func(a *NodeAbstractResourceInstance) dag.Vertex {
var m *NodeApplyableResourceInstance
// Add the config and state since we don't do that via transforms
a.Config = n.Config
a.ResolvedProvider = n.ResolvedProvider
a.Schema = n.Schema
a.ProvisionerSchemas = n.ProvisionerSchemas
a.ProviderMetas = n.ProviderMetas
a.dependsOn = n.dependsOn
a.generateConfigPath = n.generateConfigPath
m = &NodeApplyableResourceInstance{
NodeAbstractResourceInstance: a,
}
// When creating concrete instance nodes for the ephemeral resources we want to collect all the
// resourceCloser callbacks from the nodes to be able to close the resources at the end of the graph walk.
n.closers = append(n.closers, m.Close)
return m
}
// NOTE: The logic of creating a new subgraph, is also present in nodeExpandPlannableResource.
// If there will be any transformes added here, we need to double check
// the other similar implementation and mirror the changes there if needed.
// Start creating the steps
steps := []GraphTransformer{
// Expand the count or for_each (if present)
&ResourceCountTransformer{
Concrete: concreteResource,
Schema: n.Schema,
Addr: n.ResourceAddr(),
InstanceAddrs: instanceAddrs,
},
// Targeting
&TargetingTransformer{Targets: n.Targets, Excludes: n.Excludes},
// Connect references so ordering is correct
&ReferenceTransformer{},
// Make sure there is a single root
&RootTransformer{},
}
// Build the graph
b := &BasicGraphBuilder{
Steps: steps,
Name: "nodeExpandEphemeralApplyableResource",
}
graph, graphDiags := b.Build(ctx, addr.Module)
return graph, diags.Append(graphDiags).ErrWithWarnings()
}
// Close implements closableResource
func (n *nodeExpandApplyableResource) Close() (diags tfdiags.Diagnostics) {
if n.Addr.Resource.Mode != addrs.EphemeralResourceMode {
return diags
}
var wg sync.WaitGroup
diagsCh := make(chan tfdiags.Diagnostics, len(n.closers))
log.Printf("[TRACE] nodeExpandApplyableResource - scheduling %d closing operations for of ephemeral resource %s", len(n.closers), n.Addr.String())
for _, cb := range n.closers {
wg.Go(func() {
diagsCh <- cb()
})
}
wg.Wait()
close(diagsCh)
for d := range diagsCh {
diags = diags.Append(d)
}
return diags
}