mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-27 09:21:14 -06:00
509 lines
15 KiB
Go
509 lines
15 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) (diags tfdiags.Diagnostics) {
|
|
state := ctx.State()
|
|
if state == nil {
|
|
return
|
|
}
|
|
|
|
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()))
|
|
|
|
// Keep existing warnings, while converting errors to warnings.
|
|
// This is not meant to be the normal path, so there no need to
|
|
// make the errors pretty.
|
|
var warnings tfdiags.Diagnostics
|
|
for _, d := range diags {
|
|
switch d.Severity() {
|
|
case tfdiags.Warning:
|
|
warnings = warnings.Append(d)
|
|
case tfdiags.Error:
|
|
desc := d.Description()
|
|
warnings = warnings.Append(tfdiags.SimpleWarning(fmt.Sprintf("%s:%s", desc.Summary, desc.Detail)))
|
|
}
|
|
}
|
|
|
|
return warnings
|
|
}
|
|
return diags
|
|
}
|
|
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 diags
|
|
}
|
|
|
|
// 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) tfdiags.Diagnostics {
|
|
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)
|
|
}
|
|
|
|
}
|