| 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() |
| } |
| |
| if errs := objchange.AssertPlanValid(schema, priorVal, configVal, plannedNewVal); len(errs) > 0 { |
| if resp.LegacyTypeSystem { |
| // The shimming of the old type system in the legacy SDK is not precise |
| // enough to pass this consistency check, so we'll give it a pass here, |
| // but we will generate a warning about it so that we are more likely |
| // to notice in the logs if an inconsistency beyond the type system |
| // leads to a downstream provider failure. |
| var buf strings.Builder |
| fmt.Fprintf(&buf, "[WARN] Provider %q produced an invalid plan for %s, but we are tolerating it because it is using the legacy plugin SDK.\n The following problems may be the cause of any confusing errors from downstream operations:", n.ProviderAddr.ProviderConfig.Type, absAddr) |
| for _, err := range errs { |
| fmt.Fprintf(&buf, "\n - %s", tfdiags.FormatError(err)) |
| } |
| log.Print(buf.String()) |
| } else { |
| for _, err := range errs { |
| 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()), |
| ), |
| )) |
| } |
| 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. |
| continue |
| } |
| |
| priorChangedVal, priorPathDiags := hcl.ApplyPath(priorVal, path, nil) |
| plannedChangedVal, plannedPathDiags := hcl.ApplyPath(plannedNewVal, path, nil) |
| if plannedPathDiags.HasErrors() && priorPathDiags.HasErrors() { |
| // This means the path was invalid in both the prior and new |
| // values, which is an error with the provider itself. |
| 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 |
| } |
| |
| // Make sure we have valid Values for both values. |
| // Note: if the opposing value was of the type |
| // cty.DynamicPseudoType, the type assigned here may not exactly |
| // match the schema. This is fine here, since we're only going to |
| // check for equality, but if the NullVal is to be used, we need to |
| // check the schema for th true type. |
| switch { |
| case priorChangedVal == cty.NilVal && plannedChangedVal == cty.NilVal: |
| // this should never happen without ApplyPath errors above |
| panic("requires replace path returned 2 nil values") |
| case priorChangedVal == cty.NilVal: |
| priorChangedVal = cty.NullVal(plannedChangedVal.Type()) |
| case plannedChangedVal == cty.NilVal: |
| plannedChangedVal = cty.NullVal(priorChangedVal.Type()) |
| } |
| |
| 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 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%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, diags := hcl.ApplyPath(prior, path, nil) |
| if diags.HasErrors() { |
| // We just ignore the errors 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 |
| } |