mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-30 10:47:14 -06:00
28e4281674
The state is not loaded here with any marks, so we cannot rely on marks alone for equality comparison. Compare both the state and the configuration sensitivity before creating the OutputChange.
495 lines
14 KiB
Go
495 lines
14 KiB
Go
package terraform
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/terraform/addrs"
|
|
"github.com/hashicorp/terraform/configs"
|
|
"github.com/hashicorp/terraform/dag"
|
|
"github.com/hashicorp/terraform/lang"
|
|
"github.com/hashicorp/terraform/plans"
|
|
"github.com/hashicorp/terraform/states"
|
|
"github.com/hashicorp/terraform/tfdiags"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
// nodeExpandOutput is the placeholder for a non-root module output that has
|
|
// not yet had its module path expanded.
|
|
type nodeExpandOutput struct {
|
|
Addr addrs.OutputValue
|
|
Module addrs.Module
|
|
Config *configs.Output
|
|
Changes []*plans.OutputChangeSrc
|
|
Destroy bool
|
|
}
|
|
|
|
var (
|
|
_ GraphNodeReferenceable = (*nodeExpandOutput)(nil)
|
|
_ GraphNodeReferencer = (*nodeExpandOutput)(nil)
|
|
_ GraphNodeReferenceOutside = (*nodeExpandOutput)(nil)
|
|
_ GraphNodeDynamicExpandable = (*nodeExpandOutput)(nil)
|
|
_ graphNodeTemporaryValue = (*nodeExpandOutput)(nil)
|
|
_ graphNodeExpandsInstances = (*nodeExpandOutput)(nil)
|
|
)
|
|
|
|
func (n *nodeExpandOutput) expandsInstances() {}
|
|
|
|
func (n *nodeExpandOutput) temporaryValue() bool {
|
|
// non root outputs are temporary
|
|
return !n.Module.IsRoot()
|
|
}
|
|
|
|
func (n *nodeExpandOutput) DynamicExpand(ctx EvalContext) (*Graph, error) {
|
|
if n.Destroy {
|
|
// if we're planning a destroy, we only need to handle the root outputs.
|
|
// The destroy plan doesn't evaluate any other config, so we can skip
|
|
// the rest of the outputs.
|
|
return n.planDestroyRootOutput(ctx)
|
|
}
|
|
|
|
expander := ctx.InstanceExpander()
|
|
|
|
var g Graph
|
|
for _, module := range expander.ExpandModule(n.Module) {
|
|
absAddr := n.Addr.Absolute(module)
|
|
|
|
// Find any recorded change for this output
|
|
var change *plans.OutputChangeSrc
|
|
for _, c := range n.Changes {
|
|
if c.Addr.String() == absAddr.String() {
|
|
change = c
|
|
break
|
|
}
|
|
}
|
|
|
|
o := &NodeApplyableOutput{
|
|
Addr: absAddr,
|
|
Config: n.Config,
|
|
Change: change,
|
|
}
|
|
log.Printf("[TRACE] Expanding output: adding %s as %T", o.Addr.String(), o)
|
|
g.Add(o)
|
|
}
|
|
return &g, nil
|
|
}
|
|
|
|
// if we're planing a destroy operation, add a destroy node for any root output
|
|
func (n *nodeExpandOutput) planDestroyRootOutput(ctx EvalContext) (*Graph, error) {
|
|
if !n.Module.IsRoot() {
|
|
return nil, nil
|
|
}
|
|
state := ctx.State()
|
|
if state == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
var g Graph
|
|
o := &NodeDestroyableOutput{
|
|
Addr: n.Addr.Absolute(addrs.RootModuleInstance),
|
|
Config: n.Config,
|
|
}
|
|
log.Printf("[TRACE] Expanding output: adding %s as %T", o.Addr.String(), o)
|
|
g.Add(o)
|
|
|
|
return &g, nil
|
|
}
|
|
|
|
func (n *nodeExpandOutput) Name() string {
|
|
path := n.Module.String()
|
|
addr := n.Addr.String() + " (expand)"
|
|
if path != "" {
|
|
return path + "." + addr
|
|
}
|
|
return addr
|
|
}
|
|
|
|
// GraphNodeModulePath
|
|
func (n *nodeExpandOutput) ModulePath() addrs.Module {
|
|
return n.Module
|
|
}
|
|
|
|
// GraphNodeReferenceable
|
|
func (n *nodeExpandOutput) ReferenceableAddrs() []addrs.Referenceable {
|
|
// An output in the root module can't be referenced at all.
|
|
if n.Module.IsRoot() {
|
|
return nil
|
|
}
|
|
|
|
// the output is referenced through the module call, and via the
|
|
// module itself.
|
|
_, call := n.Module.Call()
|
|
callOutput := addrs.ModuleCallOutput{
|
|
Call: call,
|
|
Name: n.Addr.Name,
|
|
}
|
|
|
|
// Otherwise, we can reference the output via the
|
|
// module call itself
|
|
return []addrs.Referenceable{call, callOutput}
|
|
}
|
|
|
|
// GraphNodeReferenceOutside implementation
|
|
func (n *nodeExpandOutput) ReferenceOutside() (selfPath, referencePath addrs.Module) {
|
|
// Output values have their expressions resolved in the context of the
|
|
// module where they are defined.
|
|
referencePath = n.Module
|
|
|
|
// ...but they are referenced in the context of their calling module.
|
|
selfPath = referencePath.Parent()
|
|
|
|
return // uses named return values
|
|
}
|
|
|
|
// GraphNodeReferencer
|
|
func (n *nodeExpandOutput) References() []*addrs.Reference {
|
|
// root outputs might be destroyable, and may not reference anything in
|
|
// that case
|
|
return referencesForOutput(n.Config)
|
|
}
|
|
|
|
// NodeApplyableOutput represents an output that is "applyable":
|
|
// it is ready to be applied.
|
|
type NodeApplyableOutput struct {
|
|
Addr addrs.AbsOutputValue
|
|
Config *configs.Output // Config is the output in the config
|
|
// If this is being evaluated during apply, we may have a change recorded already
|
|
Change *plans.OutputChangeSrc
|
|
}
|
|
|
|
var (
|
|
_ GraphNodeModuleInstance = (*NodeApplyableOutput)(nil)
|
|
_ GraphNodeReferenceable = (*NodeApplyableOutput)(nil)
|
|
_ GraphNodeReferencer = (*NodeApplyableOutput)(nil)
|
|
_ GraphNodeReferenceOutside = (*NodeApplyableOutput)(nil)
|
|
_ GraphNodeExecutable = (*NodeApplyableOutput)(nil)
|
|
_ graphNodeTemporaryValue = (*NodeApplyableOutput)(nil)
|
|
_ dag.GraphNodeDotter = (*NodeApplyableOutput)(nil)
|
|
)
|
|
|
|
func (n *NodeApplyableOutput) temporaryValue() bool {
|
|
// this must always be evaluated if it is a root module output
|
|
return !n.Addr.Module.IsRoot()
|
|
}
|
|
|
|
func (n *NodeApplyableOutput) Name() string {
|
|
return n.Addr.String()
|
|
}
|
|
|
|
// GraphNodeModuleInstance
|
|
func (n *NodeApplyableOutput) Path() addrs.ModuleInstance {
|
|
return n.Addr.Module
|
|
}
|
|
|
|
// GraphNodeModulePath
|
|
func (n *NodeApplyableOutput) ModulePath() addrs.Module {
|
|
return n.Addr.Module.Module()
|
|
}
|
|
|
|
func referenceOutsideForOutput(addr addrs.AbsOutputValue) (selfPath, referencePath addrs.Module) {
|
|
// Output values have their expressions resolved in the context of the
|
|
// module where they are defined.
|
|
referencePath = addr.Module.Module()
|
|
|
|
// ...but they are referenced in the context of their calling module.
|
|
selfPath = addr.Module.Parent().Module()
|
|
|
|
return // uses named return values
|
|
}
|
|
|
|
// GraphNodeReferenceOutside implementation
|
|
func (n *NodeApplyableOutput) ReferenceOutside() (selfPath, referencePath addrs.Module) {
|
|
return referenceOutsideForOutput(n.Addr)
|
|
}
|
|
|
|
func referenceableAddrsForOutput(addr addrs.AbsOutputValue) []addrs.Referenceable {
|
|
// An output in the root module can't be referenced at all.
|
|
if addr.Module.IsRoot() {
|
|
return nil
|
|
}
|
|
|
|
// Otherwise, we can be referenced via a reference to our output name
|
|
// on the parent module's call, or via a reference to the entire call.
|
|
// e.g. module.foo.bar or just module.foo .
|
|
// Note that our ReferenceOutside method causes these addresses to be
|
|
// relative to the calling module, not the module where the output
|
|
// was declared.
|
|
_, outp := addr.ModuleCallOutput()
|
|
_, call := addr.Module.CallInstance()
|
|
|
|
return []addrs.Referenceable{outp, call}
|
|
}
|
|
|
|
// GraphNodeReferenceable
|
|
func (n *NodeApplyableOutput) ReferenceableAddrs() []addrs.Referenceable {
|
|
return referenceableAddrsForOutput(n.Addr)
|
|
}
|
|
|
|
func referencesForOutput(c *configs.Output) []*addrs.Reference {
|
|
impRefs, _ := lang.ReferencesInExpr(c.Expr)
|
|
expRefs, _ := lang.References(c.DependsOn)
|
|
l := len(impRefs) + len(expRefs)
|
|
if l == 0 {
|
|
return nil
|
|
}
|
|
refs := make([]*addrs.Reference, 0, l)
|
|
refs = append(refs, impRefs...)
|
|
refs = append(refs, expRefs...)
|
|
return refs
|
|
|
|
}
|
|
|
|
// GraphNodeReferencer
|
|
func (n *NodeApplyableOutput) References() []*addrs.Reference {
|
|
return referencesForOutput(n.Config)
|
|
}
|
|
|
|
// GraphNodeExecutable
|
|
func (n *NodeApplyableOutput) Execute(ctx EvalContext, op walkOperation) error {
|
|
var diags tfdiags.Diagnostics
|
|
state := ctx.State()
|
|
if state == nil {
|
|
return nil
|
|
}
|
|
|
|
changes := ctx.Changes() // may be nil, if we're not working on a changeset
|
|
|
|
val := cty.UnknownVal(cty.DynamicPseudoType)
|
|
changeRecorded := n.Change != nil
|
|
// we we have a change recorded, we don't need to re-evaluate if the value
|
|
// was known
|
|
if changeRecorded {
|
|
var err error
|
|
val, err = n.Change.After.Decode(cty.DynamicPseudoType)
|
|
diags = diags.Append(err)
|
|
}
|
|
|
|
// If there was no change recorded, or the recorded change was not wholly
|
|
// known, then we need to re-evaluate the output
|
|
if !changeRecorded || !val.IsWhollyKnown() {
|
|
// This has to run before we have a state lock, since evaluation also
|
|
// reads the state
|
|
val, diags = ctx.EvaluateExpr(n.Config.Expr, cty.DynamicPseudoType, nil)
|
|
// We'll handle errors below, after we have loaded the module.
|
|
// Outputs don't have a separate mode for validation, so validate
|
|
// depends_on expressions here too
|
|
diags = diags.Append(validateDependsOn(ctx, n.Config.DependsOn))
|
|
|
|
// Ensure that non-sensitive outputs don't include sensitive values
|
|
_, marks := val.UnmarkDeep()
|
|
_, hasSensitive := marks["sensitive"]
|
|
if !n.Config.Sensitive && hasSensitive {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Output refers to sensitive values",
|
|
Detail: "Expressions used in outputs can only refer to sensitive values if the sensitive attribute is true.",
|
|
Subject: n.Config.DeclRange.Ptr(),
|
|
})
|
|
}
|
|
}
|
|
|
|
// handling the interpolation error
|
|
if diags.HasErrors() {
|
|
if flagWarnOutputErrors {
|
|
log.Printf("[ERROR] Output interpolation %q failed: %s", n.Addr, diags.Err())
|
|
// if we're continuing, make sure the output is included, and
|
|
// marked as unknown. If the evaluator was able to find a type
|
|
// for the value in spite of the error then we'll use it.
|
|
n.setValue(state, changes, cty.UnknownVal(val.Type()))
|
|
return EvalEarlyExitError{}
|
|
}
|
|
return diags.Err()
|
|
}
|
|
n.setValue(state, changes, val)
|
|
|
|
// If we were able to evaluate a new value, we can update that in the
|
|
// refreshed state as well.
|
|
if state = ctx.RefreshState(); state != nil && val.IsWhollyKnown() {
|
|
n.setValue(state, changes, val)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// dag.GraphNodeDotter impl.
|
|
func (n *NodeApplyableOutput) DotNode(name string, opts *dag.DotOpts) *dag.DotNode {
|
|
return &dag.DotNode{
|
|
Name: name,
|
|
Attrs: map[string]string{
|
|
"label": n.Name(),
|
|
"shape": "note",
|
|
},
|
|
}
|
|
}
|
|
|
|
// NodeDestroyableOutput represents an output that is "destroyable":
|
|
// its application will remove the output from the state.
|
|
type NodeDestroyableOutput struct {
|
|
Addr addrs.AbsOutputValue
|
|
Config *configs.Output // Config is the output in the config
|
|
}
|
|
|
|
var (
|
|
_ GraphNodeExecutable = (*NodeDestroyableOutput)(nil)
|
|
_ dag.GraphNodeDotter = (*NodeDestroyableOutput)(nil)
|
|
)
|
|
|
|
func (n *NodeDestroyableOutput) Name() string {
|
|
return fmt.Sprintf("%s (destroy)", n.Addr.String())
|
|
}
|
|
|
|
// GraphNodeModulePath
|
|
func (n *NodeDestroyableOutput) ModulePath() addrs.Module {
|
|
return n.Addr.Module.Module()
|
|
}
|
|
|
|
func (n *NodeDestroyableOutput) temporaryValue() bool {
|
|
// this must always be evaluated if it is a root module output
|
|
return !n.Addr.Module.IsRoot()
|
|
}
|
|
|
|
// GraphNodeExecutable
|
|
func (n *NodeDestroyableOutput) Execute(ctx EvalContext, op walkOperation) error {
|
|
state := ctx.State()
|
|
if state == nil {
|
|
return nil
|
|
}
|
|
|
|
// if this is a root module, try to get a before value from the state for
|
|
// the diff
|
|
sensitiveBefore := false
|
|
before := cty.NullVal(cty.DynamicPseudoType)
|
|
mod := state.Module(n.Addr.Module)
|
|
if n.Addr.Module.IsRoot() && mod != nil {
|
|
for name, o := range mod.OutputValues {
|
|
if name == n.Addr.OutputValue.Name {
|
|
sensitiveBefore = o.Sensitive
|
|
before = o.Value
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
changes := ctx.Changes()
|
|
if changes != nil {
|
|
change := &plans.OutputChange{
|
|
Addr: n.Addr,
|
|
Sensitive: sensitiveBefore,
|
|
Change: plans.Change{
|
|
Action: plans.Delete,
|
|
Before: before,
|
|
After: cty.NullVal(cty.DynamicPseudoType),
|
|
},
|
|
}
|
|
|
|
cs, err := change.Encode()
|
|
if err != nil {
|
|
// Should never happen, since we just constructed this right above
|
|
panic(fmt.Sprintf("planned change for %s could not be encoded: %s", n.Addr, err))
|
|
}
|
|
log.Printf("[TRACE] NodeDestroyableOutput: Saving %s change for %s in changeset", change.Action, n.Addr)
|
|
changes.RemoveOutputChange(n.Addr) // remove any existing planned change, if present
|
|
changes.AppendOutputChange(cs) // add the new planned change
|
|
}
|
|
|
|
state.RemoveOutputValue(n.Addr)
|
|
return nil
|
|
}
|
|
|
|
// dag.GraphNodeDotter impl.
|
|
func (n *NodeDestroyableOutput) DotNode(name string, opts *dag.DotOpts) *dag.DotNode {
|
|
return &dag.DotNode{
|
|
Name: name,
|
|
Attrs: map[string]string{
|
|
"label": n.Name(),
|
|
"shape": "note",
|
|
},
|
|
}
|
|
}
|
|
|
|
func (n *NodeApplyableOutput) setValue(state *states.SyncState, changes *plans.ChangesSync, val cty.Value) {
|
|
// If we have an active changeset then we'll first replicate the value in
|
|
// there and lookup the prior value in the state. This is used in
|
|
// preference to the state where present, since it *is* able to represent
|
|
// unknowns, while the state cannot.
|
|
if changes != nil {
|
|
// if this is a root module, try to get a before value from the state for
|
|
// the diff
|
|
sensitiveBefore := false
|
|
before := cty.NullVal(cty.DynamicPseudoType)
|
|
mod := state.Module(n.Addr.Module)
|
|
if n.Addr.Module.IsRoot() && mod != nil {
|
|
for name, o := range mod.OutputValues {
|
|
if name == n.Addr.OutputValue.Name {
|
|
before = o.Value
|
|
sensitiveBefore = o.Sensitive
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// We will not show the value is either the before or after are marked
|
|
// as sensitivity. We can show the value again once sensitivity is
|
|
// removed from both the config and the state.
|
|
sensitiveChange := sensitiveBefore || n.Config.Sensitive
|
|
|
|
// strip any marks here just to be sure we don't panic on the True comparison
|
|
val, _ = val.UnmarkDeep()
|
|
|
|
var action plans.Action
|
|
switch {
|
|
case val.IsNull():
|
|
action = plans.Delete
|
|
|
|
case before.IsNull():
|
|
action = plans.Create
|
|
|
|
case val.IsWhollyKnown() &&
|
|
val.Equals(before).True() &&
|
|
n.Config.Sensitive == sensitiveBefore:
|
|
// Sensitivity must also match to be a NoOp.
|
|
// Theoretically marks may not match here, but sensitivity is the
|
|
// only one we can act on, and the state will have been loaded
|
|
// without any marks to consider.
|
|
action = plans.NoOp
|
|
|
|
default:
|
|
action = plans.Update
|
|
}
|
|
|
|
change := &plans.OutputChange{
|
|
Addr: n.Addr,
|
|
Sensitive: sensitiveChange,
|
|
Change: plans.Change{
|
|
Action: action,
|
|
Before: before,
|
|
After: val,
|
|
},
|
|
}
|
|
|
|
cs, err := change.Encode()
|
|
if err != nil {
|
|
// Should never happen, since we just constructed this right above
|
|
panic(fmt.Sprintf("planned change for %s could not be encoded: %s", n.Addr, err))
|
|
}
|
|
log.Printf("[TRACE] ExecuteWriteOutput: Saving %s change for %s in changeset", change.Action, n.Addr)
|
|
changes.RemoveOutputChange(n.Addr) // remove any existing planned change, if present
|
|
changes.AppendOutputChange(cs) // add the new planned change
|
|
}
|
|
|
|
if val.IsKnown() && !val.IsNull() {
|
|
// The state itself doesn't represent unknown values, so we null them
|
|
// out here and then we'll save the real unknown value in the planned
|
|
// changeset below, if we have one on this graph walk.
|
|
log.Printf("[TRACE] EvalWriteOutput: Saving value for %s in state", n.Addr)
|
|
unmarkedVal, _ := val.UnmarkDeep()
|
|
stateVal := cty.UnknownAsNull(unmarkedVal)
|
|
state.SetOutputValue(n.Addr, stateVal, n.Config.Sensitive)
|
|
} else {
|
|
log.Printf("[TRACE] EvalWriteOutput: Removing %s from state (it is now null)", n.Addr)
|
|
state.RemoveOutputValue(n.Addr)
|
|
}
|
|
|
|
}
|