mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 18:01:01 -06:00
168d84b3c4
Previously we were fetching these from the provider but then immediately discarding the version numbers because the schema API had nowhere to put them. To avoid a late-breaking change to the internal structure of terraform.ProviderSchema (which is constructed directly all over the tests) we're retaining the resource type schemas in a new map alongside the existing one with the same keys, rather than just switching to using the providers.Schema struct directly there. The methods that return resource type schemas now return two arguments, intentionally creating a little API friction here so each new caller can be reminded to think about whether they need to do something with the schema version, though it can be ignored by many callers. Since this was a breaking change to the Schemas API anyway, this also fixes another API wart where there was a separate method for fetching managed vs. data resource types and thus every caller ended up having a switch statement on "mode". Now we just accept mode as an argument and do the switch statement within the single SchemaForResourceType method.
917 lines
30 KiB
Go
917 lines
30 KiB
Go
package terraform
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"log"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl2/hcl"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/hashicorp/terraform/addrs"
|
|
"github.com/hashicorp/terraform/configs"
|
|
"github.com/hashicorp/terraform/plans"
|
|
"github.com/hashicorp/terraform/plans/objchange"
|
|
"github.com/hashicorp/terraform/providers"
|
|
"github.com/hashicorp/terraform/states"
|
|
"github.com/hashicorp/terraform/tfdiags"
|
|
)
|
|
|
|
// EvalCheckPlannedChange is an EvalNode implementation that produces errors
|
|
// if the _actual_ expected value is not compatible with what was recorded
|
|
// in the plan.
|
|
//
|
|
// Errors here are most often indicative of a bug in the provider, so our
|
|
// error messages will report with that in mind. It's also possible that
|
|
// there's a bug in Terraform's Core's own "proposed new value" code in
|
|
// EvalDiff.
|
|
type EvalCheckPlannedChange struct {
|
|
Addr addrs.ResourceInstance
|
|
ProviderAddr addrs.AbsProviderConfig
|
|
ProviderSchema **ProviderSchema
|
|
|
|
// We take ResourceInstanceChange objects here just because that's what's
|
|
// convenient to pass in from the evaltree implementation, but we really
|
|
// only look at the "After" value of each change.
|
|
Planned, Actual **plans.ResourceInstanceChange
|
|
}
|
|
|
|
func (n *EvalCheckPlannedChange) Eval(ctx EvalContext) (interface{}, error) {
|
|
providerSchema := *n.ProviderSchema
|
|
plannedChange := *n.Planned
|
|
actualChange := *n.Actual
|
|
|
|
schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource())
|
|
if schema == nil {
|
|
// Should be caught during validation, so we don't bother with a pretty error here
|
|
return nil, fmt.Errorf("provider does not support %q", n.Addr.Resource.Type)
|
|
}
|
|
|
|
var diags tfdiags.Diagnostics
|
|
absAddr := n.Addr.Absolute(ctx.Path())
|
|
|
|
log.Printf("[TRACE] EvalCheckPlannedChange: Verifying that actual change (action %s) matches planned change (action %s)", actualChange.Action, plannedChange.Action)
|
|
|
|
if plannedChange.Action != actualChange.Action {
|
|
switch {
|
|
case plannedChange.Action == plans.Update && actualChange.Action == plans.NoOp:
|
|
// It's okay for an update to become a NoOp once we've filled in
|
|
// all of the unknown values, since the final values might actually
|
|
// match what was there before after all.
|
|
log.Printf("[DEBUG] After incorporating new values learned so far during apply, %s change has become NoOp", absAddr)
|
|
default:
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Provider produced inconsistent final plan",
|
|
fmt.Sprintf(
|
|
"When expanding the plan for %s to include new values learned so far during apply, provider %q changed the planned action from %s to %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.",
|
|
absAddr, n.ProviderAddr.ProviderConfig.Type,
|
|
plannedChange.Action, actualChange.Action,
|
|
),
|
|
))
|
|
}
|
|
}
|
|
|
|
errs := objchange.AssertObjectCompatible(schema, plannedChange.After, actualChange.After)
|
|
for _, err := range errs {
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Provider produced inconsistent final plan",
|
|
fmt.Sprintf(
|
|
"When expanding the plan for %s to include new values learned so far during apply, provider %q produced an invalid new value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.",
|
|
absAddr, n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatError(err),
|
|
),
|
|
))
|
|
}
|
|
return nil, diags.Err()
|
|
}
|
|
|
|
// EvalDiff is an EvalNode implementation that detects changes for a given
|
|
// resource instance.
|
|
type EvalDiff struct {
|
|
Addr addrs.ResourceInstance
|
|
Config *configs.Resource
|
|
Provider *providers.Interface
|
|
ProviderAddr addrs.AbsProviderConfig
|
|
ProviderSchema **ProviderSchema
|
|
State **states.ResourceInstanceObject
|
|
PreviousDiff **plans.ResourceInstanceChange
|
|
|
|
// CreateBeforeDestroy is set if either the resource's own config sets
|
|
// create_before_destroy explicitly or if dependencies have forced the
|
|
// resource to be handled as create_before_destroy in order to avoid
|
|
// a dependency cycle.
|
|
CreateBeforeDestroy bool
|
|
|
|
OutputChange **plans.ResourceInstanceChange
|
|
OutputValue *cty.Value
|
|
OutputState **states.ResourceInstanceObject
|
|
|
|
Stub bool
|
|
}
|
|
|
|
// TODO: test
|
|
func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
|
|
state := *n.State
|
|
config := *n.Config
|
|
provider := *n.Provider
|
|
providerSchema := *n.ProviderSchema
|
|
|
|
if providerSchema == nil {
|
|
return nil, fmt.Errorf("provider schema is unavailable for %s", n.Addr)
|
|
}
|
|
if n.ProviderAddr.ProviderConfig.Type == "" {
|
|
panic(fmt.Sprintf("EvalDiff for %s does not have ProviderAddr set", n.Addr.Absolute(ctx.Path())))
|
|
}
|
|
|
|
var diags tfdiags.Diagnostics
|
|
|
|
// Evaluate the configuration
|
|
schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource())
|
|
if schema == nil {
|
|
// Should be caught during validation, so we don't bother with a pretty error here
|
|
return nil, fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Type)
|
|
}
|
|
keyData := EvalDataForInstanceKey(n.Addr.Key)
|
|
configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData)
|
|
diags = diags.Append(configDiags)
|
|
if configDiags.HasErrors() {
|
|
return nil, diags.Err()
|
|
}
|
|
|
|
absAddr := n.Addr.Absolute(ctx.Path())
|
|
var priorVal cty.Value
|
|
var priorValTainted cty.Value
|
|
var priorPrivate []byte
|
|
if state != nil {
|
|
if state.Status != states.ObjectTainted {
|
|
priorVal = state.Value
|
|
priorPrivate = state.Private
|
|
} else {
|
|
// If the prior state is tainted then we'll proceed below like
|
|
// we're creating an entirely new object, but then turn it into
|
|
// a synthetic "Replace" change at the end, creating the same
|
|
// result as if the provider had marked at least one argument
|
|
// change as "requires replacement".
|
|
priorValTainted = state.Value
|
|
priorVal = cty.NullVal(schema.ImpliedType())
|
|
}
|
|
} else {
|
|
priorVal = cty.NullVal(schema.ImpliedType())
|
|
}
|
|
|
|
proposedNewVal := objchange.ProposedNewObject(schema, priorVal, configVal)
|
|
|
|
// Call pre-diff hook
|
|
if !n.Stub {
|
|
err := ctx.Hook(func(h Hook) (HookAction, error) {
|
|
return h.PreDiff(absAddr, states.CurrentGen, priorVal, proposedNewVal)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// The provider gets an opportunity to customize the proposed new value,
|
|
// which in turn produces the _planned_ new value.
|
|
resp := provider.PlanResourceChange(providers.PlanResourceChangeRequest{
|
|
TypeName: n.Addr.Resource.Type,
|
|
Config: configVal,
|
|
PriorState: priorVal,
|
|
ProposedNewState: proposedNewVal,
|
|
PriorPrivate: priorPrivate,
|
|
})
|
|
diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config))
|
|
if diags.HasErrors() {
|
|
return nil, diags.Err()
|
|
}
|
|
|
|
plannedNewVal := resp.PlannedState
|
|
plannedPrivate := resp.PlannedPrivate
|
|
|
|
if plannedNewVal == cty.NilVal {
|
|
// Should never happen. Since real-world providers return via RPC a nil
|
|
// is always a bug in the client-side stub. This is more likely caused
|
|
// by an incompletely-configured mock provider in tests, though.
|
|
panic(fmt.Sprintf("PlanResourceChange of %s produced nil value", absAddr.String()))
|
|
}
|
|
|
|
// We allow the planned new value to disagree with configuration _values_
|
|
// here, since that allows the provider to do special logic like a
|
|
// DiffSuppressFunc, but we still require that the provider produces
|
|
// a value whose type conforms to the schema.
|
|
for _, err := range plannedNewVal.Type().TestConformance(schema.ImpliedType()) {
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Provider produced invalid plan",
|
|
fmt.Sprintf(
|
|
"Provider %q planned an invalid value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.",
|
|
n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatErrorPrefixed(err, absAddr.String()),
|
|
),
|
|
))
|
|
}
|
|
if diags.HasErrors() {
|
|
return nil, diags.Err()
|
|
}
|
|
|
|
{
|
|
var moreDiags tfdiags.Diagnostics
|
|
plannedNewVal, moreDiags = n.processIgnoreChanges(priorVal, plannedNewVal)
|
|
diags = diags.Append(moreDiags)
|
|
if moreDiags.HasErrors() {
|
|
return nil, diags.Err()
|
|
}
|
|
}
|
|
|
|
// The provider produces a list of paths to attributes whose changes mean
|
|
// that we must replace rather than update an existing remote object.
|
|
// However, we only need to do that if the identified attributes _have_
|
|
// actually changed -- particularly after we may have undone some of the
|
|
// changes in processIgnoreChanges -- so now we'll filter that list to
|
|
// include only where changes are detected.
|
|
reqRep := cty.NewPathSet()
|
|
if len(resp.RequiresReplace) > 0 {
|
|
for _, path := range resp.RequiresReplace {
|
|
if priorVal.IsNull() {
|
|
// If prior is null then we don't expect any RequiresReplace at all,
|
|
// because this is a Create action. (This is just to avoid errors
|
|
// when we use this value below, if the provider misbehaves.)
|
|
continue
|
|
}
|
|
plannedChangedVal, err := path.Apply(plannedNewVal)
|
|
if err != nil {
|
|
// This always indicates a provider bug, since RequiresReplace
|
|
// should always refer only to whole attributes (and not into
|
|
// attribute values themselves) and these should always be
|
|
// present, even though they might be null or unknown.
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Provider produced invalid plan",
|
|
fmt.Sprintf(
|
|
"Provider %q has indicated \"requires replacement\" on %s for a non-existent attribute path %#v.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.",
|
|
n.ProviderAddr.ProviderConfig.Type, absAddr, path,
|
|
),
|
|
))
|
|
continue
|
|
}
|
|
priorChangedVal, err := path.Apply(priorVal)
|
|
if err != nil {
|
|
// Should never happen since prior and changed should be of
|
|
// the same type, but we'll allow it for robustness.
|
|
reqRep.Add(path)
|
|
}
|
|
if priorChangedVal != cty.NilVal {
|
|
eqV := plannedChangedVal.Equals(priorChangedVal)
|
|
if !eqV.IsKnown() || eqV.False() {
|
|
reqRep.Add(path)
|
|
}
|
|
}
|
|
}
|
|
if diags.HasErrors() {
|
|
return nil, diags.Err()
|
|
}
|
|
}
|
|
|
|
eqV := plannedNewVal.Equals(priorVal)
|
|
eq := eqV.IsKnown() && eqV.True()
|
|
|
|
var action plans.Action
|
|
switch {
|
|
case priorVal.IsNull():
|
|
action = plans.Create
|
|
case eq:
|
|
action = plans.NoOp
|
|
case !reqRep.Empty():
|
|
// If there are any "requires replace" paths left _after our filtering
|
|
// above_ then this is a replace action.
|
|
if n.CreateBeforeDestroy {
|
|
action = plans.CreateThenDelete
|
|
} else {
|
|
action = plans.DeleteThenCreate
|
|
}
|
|
default:
|
|
action = plans.Update
|
|
// "Delete" is never chosen here, because deletion plans are always
|
|
// created more directly elsewhere, such as in "orphan" handling.
|
|
}
|
|
|
|
if action.IsReplace() {
|
|
// In this strange situation we want to produce a change object that
|
|
// shows our real prior object but has a _new_ object that is built
|
|
// from a null prior object, since we're going to delete the one
|
|
// that has all the computed values on it.
|
|
//
|
|
// Therefore we'll ask the provider to plan again here, giving it
|
|
// a null object for the prior, and then we'll meld that with the
|
|
// _actual_ prior state to produce a correctly-shaped replace change.
|
|
// The resulting change should show any computed attributes changing
|
|
// from known prior values to unknown values, unless the provider is
|
|
// able to predict new values for any of these computed attributes.
|
|
nullPriorVal := cty.NullVal(schema.ImpliedType())
|
|
|
|
// create a new proposed value from the null state and the config
|
|
proposedNewVal = objchange.ProposedNewObject(schema, nullPriorVal, configVal)
|
|
|
|
resp = provider.PlanResourceChange(providers.PlanResourceChangeRequest{
|
|
TypeName: n.Addr.Resource.Type,
|
|
Config: configVal,
|
|
PriorState: nullPriorVal,
|
|
ProposedNewState: proposedNewVal,
|
|
PriorPrivate: plannedPrivate,
|
|
})
|
|
// We need to tread carefully here, since if there are any warnings
|
|
// in here they probably also came out of our previous call to
|
|
// PlanResourceChange above, and so we don't want to repeat them.
|
|
// Consequently, we break from the usual pattern here and only
|
|
// append these new diagnostics if there's at least one error inside.
|
|
if resp.Diagnostics.HasErrors() {
|
|
diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config))
|
|
return nil, diags.Err()
|
|
}
|
|
plannedNewVal = resp.PlannedState
|
|
plannedPrivate = resp.PlannedPrivate
|
|
for _, err := range schema.ImpliedType().TestConformance(plannedNewVal.Type()) {
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Provider produced invalid plan",
|
|
fmt.Sprintf(
|
|
"Provider %q planned an invalid value for %s%s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.",
|
|
n.ProviderAddr.ProviderConfig.Type, absAddr, tfdiags.FormatError(err),
|
|
),
|
|
))
|
|
}
|
|
if diags.HasErrors() {
|
|
return nil, diags.Err()
|
|
}
|
|
}
|
|
|
|
// If our prior value was tainted then we actually want this to appear
|
|
// as a replace change, even though so far we've been treating it as a
|
|
// create.
|
|
if action == plans.Create && priorValTainted != cty.NilVal {
|
|
if n.CreateBeforeDestroy {
|
|
action = plans.CreateThenDelete
|
|
} else {
|
|
action = plans.DeleteThenCreate
|
|
}
|
|
priorVal = priorValTainted
|
|
}
|
|
|
|
// As a special case, if we have a previous diff (presumably from the plan
|
|
// phases, whereas we're now in the apply phase) and it was for a replace,
|
|
// we've already deleted the original object from state by the time we
|
|
// get here and so we would've ended up with a _create_ action this time,
|
|
// which we now need to paper over to get a result consistent with what
|
|
// we originally intended.
|
|
if n.PreviousDiff != nil {
|
|
prevChange := *n.PreviousDiff
|
|
if prevChange.Action.IsReplace() && action == plans.Create {
|
|
log.Printf("[TRACE] EvalDiff: %s treating Create change as %s change to match with earlier plan", absAddr, prevChange.Action)
|
|
action = prevChange.Action
|
|
priorVal = prevChange.Before
|
|
}
|
|
}
|
|
|
|
// Call post-refresh hook
|
|
if !n.Stub {
|
|
err := ctx.Hook(func(h Hook) (HookAction, error) {
|
|
return h.PostDiff(absAddr, states.CurrentGen, action, priorVal, plannedNewVal)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Update our output if we care
|
|
if n.OutputChange != nil {
|
|
*n.OutputChange = &plans.ResourceInstanceChange{
|
|
Addr: absAddr,
|
|
Private: plannedPrivate,
|
|
ProviderAddr: n.ProviderAddr,
|
|
Change: plans.Change{
|
|
Action: action,
|
|
Before: priorVal,
|
|
After: plannedNewVal,
|
|
},
|
|
RequiredReplace: reqRep,
|
|
}
|
|
}
|
|
|
|
if n.OutputValue != nil {
|
|
*n.OutputValue = configVal
|
|
}
|
|
|
|
// Update the state if we care
|
|
if n.OutputState != nil {
|
|
*n.OutputState = &states.ResourceInstanceObject{
|
|
// We use the special "planned" status here to note that this
|
|
// object's value is not yet complete. Objects with this status
|
|
// cannot be used during expression evaluation, so the caller
|
|
// must _also_ record the returned change in the active plan,
|
|
// which the expression evaluator will use in preference to this
|
|
// incomplete value recorded in the state.
|
|
Status: states.ObjectPlanned,
|
|
Value: plannedNewVal,
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (n *EvalDiff) processIgnoreChanges(prior, proposed cty.Value) (cty.Value, tfdiags.Diagnostics) {
|
|
// ignore_changes only applies when an object already exists, since we
|
|
// can't ignore changes to a thing we've not created yet.
|
|
if prior.IsNull() {
|
|
return proposed, nil
|
|
}
|
|
|
|
ignoreChanges := n.Config.Managed.IgnoreChanges
|
|
ignoreAll := n.Config.Managed.IgnoreAllChanges
|
|
|
|
if len(ignoreChanges) == 0 && !ignoreAll {
|
|
return proposed, nil
|
|
}
|
|
if ignoreAll {
|
|
return prior, nil
|
|
}
|
|
if prior.IsNull() || proposed.IsNull() {
|
|
// Ignore changes doesn't apply when we're creating for the first time.
|
|
// Proposed should never be null here, but if it is then we'll just let it be.
|
|
return proposed, nil
|
|
}
|
|
|
|
return processIgnoreChangesIndividual(prior, proposed, ignoreChanges)
|
|
}
|
|
|
|
func processIgnoreChangesIndividual(prior, proposed cty.Value, ignoreChanges []hcl.Traversal) (cty.Value, tfdiags.Diagnostics) {
|
|
// When we walk below we will be using cty.Path values for comparison, so
|
|
// we'll convert our traversals here so we can compare more easily.
|
|
ignoreChangesPath := make([]cty.Path, len(ignoreChanges))
|
|
for i, traversal := range ignoreChanges {
|
|
path := make(cty.Path, len(traversal))
|
|
for si, step := range traversal {
|
|
switch ts := step.(type) {
|
|
case hcl.TraverseRoot:
|
|
path[si] = cty.GetAttrStep{
|
|
Name: ts.Name,
|
|
}
|
|
case hcl.TraverseAttr:
|
|
path[si] = cty.GetAttrStep{
|
|
Name: ts.Name,
|
|
}
|
|
case hcl.TraverseIndex:
|
|
path[si] = cty.IndexStep{
|
|
Key: ts.Key,
|
|
}
|
|
default:
|
|
panic(fmt.Sprintf("unsupported traversal step %#v", step))
|
|
}
|
|
}
|
|
ignoreChangesPath[i] = path
|
|
}
|
|
|
|
var diags tfdiags.Diagnostics
|
|
ret, _ := cty.Transform(proposed, func(path cty.Path, v cty.Value) (cty.Value, error) {
|
|
// First we must see if this is a path that's being ignored at all.
|
|
// We're looking for an exact match here because this walk will visit
|
|
// leaf values first and then their containers, and we want to do
|
|
// the "ignore" transform once we reach the point indicated, throwing
|
|
// away any deeper values we already produced at that point.
|
|
var ignoreTraversal hcl.Traversal
|
|
for i, candidate := range ignoreChangesPath {
|
|
if reflect.DeepEqual(path, candidate) {
|
|
ignoreTraversal = ignoreChanges[i]
|
|
}
|
|
}
|
|
if ignoreTraversal == nil {
|
|
return v, nil
|
|
}
|
|
|
|
// If we're able to follow the same path through the prior value,
|
|
// we'll take the value there instead, effectively undoing the
|
|
// change that was planned.
|
|
priorV, err := path.Apply(prior)
|
|
if err != nil {
|
|
// We just ignore the error and move on here, since we assume it's
|
|
// just because the prior value was a slightly-different shape.
|
|
// It could potentially also be that the traversal doesn't match
|
|
// the schema, but we should've caught that during the validate
|
|
// walk if so.
|
|
return v, nil
|
|
}
|
|
return priorV, nil
|
|
})
|
|
return ret, diags
|
|
}
|
|
|
|
func (n *EvalDiff) processIgnoreChangesOld(diff *InstanceDiff) error {
|
|
if diff == nil || n.Config == nil || n.Config.Managed == nil {
|
|
return nil
|
|
}
|
|
ignoreChanges := n.Config.Managed.IgnoreChanges
|
|
ignoreAll := n.Config.Managed.IgnoreAllChanges
|
|
|
|
if len(ignoreChanges) == 0 && !ignoreAll {
|
|
return nil
|
|
}
|
|
|
|
// If we're just creating the resource, we shouldn't alter the
|
|
// Diff at all
|
|
if diff.ChangeType() == DiffCreate {
|
|
return nil
|
|
}
|
|
|
|
// If the resource has been tainted then we don't process ignore changes
|
|
// since we MUST recreate the entire resource.
|
|
if diff.GetDestroyTainted() {
|
|
return nil
|
|
}
|
|
|
|
attrs := diff.CopyAttributes()
|
|
|
|
// get the complete set of keys we want to ignore
|
|
ignorableAttrKeys := make(map[string]bool)
|
|
for k := range attrs {
|
|
if ignoreAll {
|
|
ignorableAttrKeys[k] = true
|
|
continue
|
|
}
|
|
for _, ignoredTraversal := range ignoreChanges {
|
|
ignoredKey := legacyFlatmapKeyForTraversal(ignoredTraversal)
|
|
if k == ignoredKey || strings.HasPrefix(k, ignoredKey+".") {
|
|
ignorableAttrKeys[k] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the resource was being destroyed, check to see if we can ignore the
|
|
// reason for it being destroyed.
|
|
if diff.GetDestroy() {
|
|
for k, v := range attrs {
|
|
if k == "id" {
|
|
// id will always be changed if we intended to replace this instance
|
|
continue
|
|
}
|
|
if v.Empty() || v.NewComputed {
|
|
continue
|
|
}
|
|
|
|
// If any RequiresNew attribute isn't ignored, we need to keep the diff
|
|
// as-is to be able to replace the resource.
|
|
if v.RequiresNew && !ignorableAttrKeys[k] {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Now that we know that we aren't replacing the instance, we can filter
|
|
// out all the empty and computed attributes. There may be a bunch of
|
|
// extraneous attribute diffs for the other non-requires-new attributes
|
|
// going from "" -> "configval" or "" -> "<computed>".
|
|
// We must make sure any flatmapped containers are filterred (or not) as a
|
|
// whole.
|
|
containers := groupContainers(diff)
|
|
keep := map[string]bool{}
|
|
for _, v := range containers {
|
|
if v.keepDiff(ignorableAttrKeys) {
|
|
// At least one key has changes, so list all the sibling keys
|
|
// to keep in the diff
|
|
for k := range v {
|
|
keep[k] = true
|
|
// this key may have been added by the user to ignore, but
|
|
// if it's a subkey in a container, we need to un-ignore it
|
|
// to keep the complete containter.
|
|
delete(ignorableAttrKeys, k)
|
|
}
|
|
}
|
|
}
|
|
|
|
for k, v := range attrs {
|
|
if (v.Empty() || v.NewComputed) && !keep[k] {
|
|
ignorableAttrKeys[k] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Here we undo the two reactions to RequireNew in EvalDiff - the "id"
|
|
// attribute diff and the Destroy boolean field
|
|
log.Printf("[DEBUG] Removing 'id' diff and setting Destroy to false " +
|
|
"because after ignore_changes, this diff no longer requires replacement")
|
|
diff.DelAttribute("id")
|
|
diff.SetDestroy(false)
|
|
|
|
// If we didn't hit any of our early exit conditions, we can filter the diff.
|
|
for k := range ignorableAttrKeys {
|
|
log.Printf("[DEBUG] [EvalIgnoreChanges] %s: Ignoring diff attribute: %s", n.Addr.String(), k)
|
|
diff.DelAttribute(k)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// legacyFlagmapKeyForTraversal constructs a key string compatible with what
|
|
// the flatmap package would generate for an attribute addressable by the given
|
|
// traversal.
|
|
//
|
|
// This is used only to shim references to attributes within the diff and
|
|
// state structures, which have not (at the time of writing) yet been updated
|
|
// to use the newer HCL-based representations.
|
|
func legacyFlatmapKeyForTraversal(traversal hcl.Traversal) string {
|
|
var buf bytes.Buffer
|
|
first := true
|
|
for _, step := range traversal {
|
|
if !first {
|
|
buf.WriteByte('.')
|
|
}
|
|
switch ts := step.(type) {
|
|
case hcl.TraverseRoot:
|
|
buf.WriteString(ts.Name)
|
|
case hcl.TraverseAttr:
|
|
buf.WriteString(ts.Name)
|
|
case hcl.TraverseIndex:
|
|
val := ts.Key
|
|
switch val.Type() {
|
|
case cty.Number:
|
|
bf := val.AsBigFloat()
|
|
buf.WriteString(bf.String())
|
|
case cty.String:
|
|
s := val.AsString()
|
|
buf.WriteString(s)
|
|
default:
|
|
// should never happen, since no other types appear in
|
|
// traversals in practice.
|
|
buf.WriteByte('?')
|
|
}
|
|
default:
|
|
// should never happen, since we've covered all of the types
|
|
// that show up in parsed traversals in practice.
|
|
buf.WriteByte('?')
|
|
}
|
|
first = false
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
// a group of key-*ResourceAttrDiff pairs from the same flatmapped container
|
|
type flatAttrDiff map[string]*ResourceAttrDiff
|
|
|
|
// we need to keep all keys if any of them have a diff that's not ignored
|
|
func (f flatAttrDiff) keepDiff(ignoreChanges map[string]bool) bool {
|
|
for k, v := range f {
|
|
ignore := false
|
|
for attr := range ignoreChanges {
|
|
if strings.HasPrefix(k, attr) {
|
|
ignore = true
|
|
}
|
|
}
|
|
|
|
if !v.Empty() && !v.NewComputed && !ignore {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// sets, lists and maps need to be compared for diff inclusion as a whole, so
|
|
// group the flatmapped keys together for easier comparison.
|
|
func groupContainers(d *InstanceDiff) map[string]flatAttrDiff {
|
|
isIndex := multiVal.MatchString
|
|
containers := map[string]flatAttrDiff{}
|
|
attrs := d.CopyAttributes()
|
|
// we need to loop once to find the index key
|
|
for k := range attrs {
|
|
if isIndex(k) {
|
|
// add the key, always including the final dot to fully qualify it
|
|
containers[k[:len(k)-1]] = flatAttrDiff{}
|
|
}
|
|
}
|
|
|
|
// loop again to find all the sub keys
|
|
for prefix, values := range containers {
|
|
for k, attrDiff := range attrs {
|
|
// we include the index value as well, since it could be part of the diff
|
|
if strings.HasPrefix(k, prefix) {
|
|
values[k] = attrDiff
|
|
}
|
|
}
|
|
}
|
|
|
|
return containers
|
|
}
|
|
|
|
// EvalDiffDestroy is an EvalNode implementation that returns a plain
|
|
// destroy diff.
|
|
type EvalDiffDestroy struct {
|
|
Addr addrs.ResourceInstance
|
|
DeposedKey states.DeposedKey
|
|
State **states.ResourceInstanceObject
|
|
ProviderAddr addrs.AbsProviderConfig
|
|
|
|
Output **plans.ResourceInstanceChange
|
|
OutputState **states.ResourceInstanceObject
|
|
}
|
|
|
|
// TODO: test
|
|
func (n *EvalDiffDestroy) Eval(ctx EvalContext) (interface{}, error) {
|
|
absAddr := n.Addr.Absolute(ctx.Path())
|
|
state := *n.State
|
|
|
|
if n.ProviderAddr.ProviderConfig.Type == "" {
|
|
if n.DeposedKey == "" {
|
|
panic(fmt.Sprintf("EvalDiffDestroy for %s does not have ProviderAddr set", absAddr))
|
|
} else {
|
|
panic(fmt.Sprintf("EvalDiffDestroy for %s (deposed %s) does not have ProviderAddr set", absAddr, n.DeposedKey))
|
|
}
|
|
}
|
|
|
|
// If there is no state or our attributes object is null then we're already
|
|
// destroyed.
|
|
if state == nil || state.Value.IsNull() {
|
|
return nil, nil
|
|
}
|
|
|
|
// Call pre-diff hook
|
|
err := ctx.Hook(func(h Hook) (HookAction, error) {
|
|
return h.PreDiff(
|
|
absAddr, n.DeposedKey.Generation(),
|
|
state.Value,
|
|
cty.NullVal(cty.DynamicPseudoType),
|
|
)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Change is always the same for a destroy. We don't need the provider's
|
|
// help for this one.
|
|
// TODO: Should we give the provider an opportunity to veto this?
|
|
change := &plans.ResourceInstanceChange{
|
|
Addr: absAddr,
|
|
DeposedKey: n.DeposedKey,
|
|
Change: plans.Change{
|
|
Action: plans.Delete,
|
|
Before: state.Value,
|
|
After: cty.NullVal(cty.DynamicPseudoType),
|
|
},
|
|
ProviderAddr: n.ProviderAddr,
|
|
}
|
|
|
|
// Call post-diff hook
|
|
err = ctx.Hook(func(h Hook) (HookAction, error) {
|
|
return h.PostDiff(
|
|
absAddr,
|
|
n.DeposedKey.Generation(),
|
|
change.Action,
|
|
change.Before,
|
|
change.After,
|
|
)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Update our output
|
|
*n.Output = change
|
|
|
|
if n.OutputState != nil {
|
|
// Record our proposed new state, which is nil because we're destroying.
|
|
*n.OutputState = nil
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// EvalReduceDiff is an EvalNode implementation that takes a planned resource
|
|
// instance change as might be produced by EvalDiff or EvalDiffDestroy and
|
|
// "simplifies" it to a single atomic action to be performed by a specific
|
|
// graph node.
|
|
//
|
|
// Callers must specify whether they are a destroy node or a regular apply
|
|
// node. If the result is NoOp then the given change requires no action for
|
|
// the specific graph node calling this and so evaluation of the that graph
|
|
// node should exit early and take no action.
|
|
//
|
|
// The object written to OutChange may either be identical to InChange or
|
|
// a new change object derived from InChange. Because of the former case, the
|
|
// caller must not mutate the object returned in OutChange.
|
|
type EvalReduceDiff struct {
|
|
Addr addrs.ResourceInstance
|
|
InChange **plans.ResourceInstanceChange
|
|
Destroy bool
|
|
OutChange **plans.ResourceInstanceChange
|
|
}
|
|
|
|
// TODO: test
|
|
func (n *EvalReduceDiff) Eval(ctx EvalContext) (interface{}, error) {
|
|
in := *n.InChange
|
|
out := in.Simplify(n.Destroy)
|
|
if n.OutChange != nil {
|
|
*n.OutChange = out
|
|
}
|
|
if out.Action != in.Action {
|
|
if n.Destroy {
|
|
log.Printf("[TRACE] EvalReduceDiff: %s change simplified from %s to %s for destroy node", n.Addr, in.Action, out.Action)
|
|
} else {
|
|
log.Printf("[TRACE] EvalReduceDiff: %s change simplified from %s to %s for apply node", n.Addr, in.Action, out.Action)
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// EvalReadDiff is an EvalNode implementation that retrieves the planned
|
|
// change for a particular resource instance object.
|
|
type EvalReadDiff struct {
|
|
Addr addrs.ResourceInstance
|
|
DeposedKey states.DeposedKey
|
|
ProviderSchema **ProviderSchema
|
|
Change **plans.ResourceInstanceChange
|
|
}
|
|
|
|
func (n *EvalReadDiff) Eval(ctx EvalContext) (interface{}, error) {
|
|
providerSchema := *n.ProviderSchema
|
|
changes := ctx.Changes()
|
|
addr := n.Addr.Absolute(ctx.Path())
|
|
|
|
schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource())
|
|
if schema == nil {
|
|
// Should be caught during validation, so we don't bother with a pretty error here
|
|
return nil, fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Type)
|
|
}
|
|
|
|
gen := states.CurrentGen
|
|
if n.DeposedKey != states.NotDeposed {
|
|
gen = n.DeposedKey
|
|
}
|
|
csrc := changes.GetResourceInstanceChange(addr, gen)
|
|
if csrc == nil {
|
|
log.Printf("[TRACE] EvalReadDiff: No planned change recorded for %s", addr)
|
|
return nil, nil
|
|
}
|
|
|
|
change, err := csrc.Decode(schema.ImpliedType())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode planned changes for %s: %s", addr, err)
|
|
}
|
|
if n.Change != nil {
|
|
*n.Change = change
|
|
}
|
|
|
|
log.Printf("[TRACE] EvalReadDiff: Read %s change from plan for %s", change.Action, addr)
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// EvalWriteDiff is an EvalNode implementation that saves a planned change
|
|
// for an instance object into the set of global planned changes.
|
|
type EvalWriteDiff struct {
|
|
Addr addrs.ResourceInstance
|
|
DeposedKey states.DeposedKey
|
|
ProviderSchema **ProviderSchema
|
|
Change **plans.ResourceInstanceChange
|
|
}
|
|
|
|
// TODO: test
|
|
func (n *EvalWriteDiff) Eval(ctx EvalContext) (interface{}, error) {
|
|
changes := ctx.Changes()
|
|
addr := n.Addr.Absolute(ctx.Path())
|
|
if n.Change == nil || *n.Change == nil {
|
|
// Caller sets nil to indicate that we need to remove a change from
|
|
// the set of changes.
|
|
gen := states.CurrentGen
|
|
if n.DeposedKey != states.NotDeposed {
|
|
gen = n.DeposedKey
|
|
}
|
|
changes.RemoveResourceInstanceChange(addr, gen)
|
|
return nil, nil
|
|
}
|
|
|
|
providerSchema := *n.ProviderSchema
|
|
change := *n.Change
|
|
|
|
if change.Addr.String() != addr.String() || change.DeposedKey != n.DeposedKey {
|
|
// Should never happen, and indicates a bug in the caller.
|
|
panic("inconsistent address and/or deposed key in EvalWriteDiff")
|
|
}
|
|
|
|
schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource())
|
|
if schema == nil {
|
|
// Should be caught during validation, so we don't bother with a pretty error here
|
|
return nil, fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Type)
|
|
}
|
|
|
|
csrc, err := change.Encode(schema.ImpliedType())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to encode planned changes for %s: %s", addr, err)
|
|
}
|
|
|
|
changes.AppendResourceInstanceChange(csrc)
|
|
if n.DeposedKey == states.NotDeposed {
|
|
log.Printf("[TRACE] EvalWriteDiff: recorded %s change for %s", change.Action, addr)
|
|
} else {
|
|
log.Printf("[TRACE] EvalWriteDiff: recorded %s change for %s deposed object %s", change.Action, addr, n.DeposedKey)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|