-
Notifications
You must be signed in to change notification settings - Fork 232
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
helper/schema: Schema.DiffSuppressOnRefresh #882
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
```release-note:note | ||
helper/schema: The `Schema` type `DiffSuppressOnRefresh` field opts in to using `DiffSuppressFunc` to detect normalization changes during refresh, using the same rules as for planning. This can prevent normalization cascading downstream and producing confusing changes in other resources, and will avoid reporting "Values changed outside of Terraform" for normalization-only situations. This is a desirable behavior for most attributes that have `DiffSuppressFunc` and so would ideally be on by default, but it is opt-in for backward compatibility reasons. | ||
``` | ||
|
||
```release-note:enhancement | ||
helper/schema: Added the `DiffSuppressOnRefresh` field to the `Schema` type | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,6 +23,7 @@ import ( | |
"strings" | ||
|
||
"github.com/hashicorp/go-cty/cty" | ||
"github.com/hashicorp/terraform-plugin-log/tfsdklog" | ||
"github.com/mitchellh/copystructure" | ||
"github.com/mitchellh/mapstructure" | ||
|
||
|
@@ -92,8 +93,42 @@ type Schema struct { | |
// If CustomizeDiffFunc makes this field ForceNew=true, the | ||
// following DiffSuppressFunc will come in with the value of old being | ||
// empty, as if creating a new resource. | ||
// | ||
// By default, DiffSuppressFunc is considered only when deciding whether | ||
// a configuration value is significantly different than the prior state | ||
// value during planning. Set DiffSuppressOnRefresh to opt in to checking | ||
// this also during the refresh step. | ||
DiffSuppressFunc SchemaDiffSuppressFunc | ||
|
||
// DiffSuppressOnRefresh enables using the DiffSuppressFunc to ignore | ||
// normalization-classified changes returned by the resource type's | ||
// "Read" or "ReadContext" function, in addition to the default behavior of | ||
// doing so during planning. | ||
// | ||
// This is a particularly good choice for attributes which take strings | ||
// containing "microsyntaxes" where various different values are packed | ||
// together in some serialization where there are many ways to express the | ||
// same information. For example, attributes which accept JSON data can | ||
// include different whitespace characters without changing meaning, and | ||
// case-insensitive identifiers may refer to the same object using different | ||
// characters. | ||
// | ||
// This is valid only for attributes of primitive types, because | ||
// DiffSuppressFunc itself is only compatible with primitive types. | ||
// | ||
// The key benefit of activating this flag is that the result of Read or | ||
// ReadContext will be cleaned of normalization-only changes in the same | ||
// way as the planning result would normaly be, which therefore prevents | ||
// churn for downstream expressions deriving from this attribute and | ||
// prevents incorrect "Values changed outside of Terraform" messages | ||
// when the remote API returns values which have the same meaning as the | ||
// prior state but in a different serialization. | ||
// | ||
// This is an opt-in because it was a later addition to the DiffSuppressFunc | ||
// functionality which would cause some significant changes in behavior | ||
// for existing providers if activated everywhere all at once. | ||
DiffSuppressOnRefresh bool | ||
|
||
// If this is non-nil, then this will be a default value that is used | ||
// when this item is not set in the configuration. | ||
// | ||
|
@@ -767,6 +802,10 @@ func (m schemaMap) internalValidate(topSchemaMap schemaMap, attrsOnly bool) erro | |
} | ||
} | ||
|
||
if v.DiffSuppressOnRefresh && v.DiffSuppressFunc == nil { | ||
return fmt.Errorf("%s: cannot set DiffSuppressOnRefresh without DiffSuppressFunc", k) | ||
} | ||
|
||
if v.Type == TypeList || v.Type == TypeSet { | ||
if v.Elem == nil { | ||
return fmt.Errorf("%s: Elem must be set for lists", k) | ||
|
@@ -991,6 +1030,7 @@ func (m schemaMap) diff( | |
continue | ||
} | ||
|
||
log.Printf("[DEBUG] ignoring change of %q due to DiffSuppressFunc", attrK) | ||
attrV = &terraform.ResourceAttrDiff{ | ||
Old: attrV.Old, | ||
New: attrV.Old, | ||
|
@@ -1437,6 +1477,60 @@ func (m schemaMap) diffString( | |
return nil | ||
} | ||
|
||
// handleDiffSuppressOnRefresh visits each of the attributes set in "new" and, | ||
// if the corresponding schema sets both DiffSuppressFunc and | ||
// DiffSuppressOnRefresh, checks whether the new value is materially different | ||
// than the old and if not it overwrites the new value with the old one, | ||
// in-place. | ||
func (m schemaMap) handleDiffSuppressOnRefresh(ctx context.Context, oldState, newState *terraform.InstanceState) { | ||
if newState == nil || oldState == nil { | ||
return // nothing to do, then | ||
} | ||
|
||
// We'll populate this in the loop below only if we find at least one | ||
// attribute which needs this analysis. | ||
var d *ResourceData | ||
|
||
oldAttrs := oldState.Attributes | ||
newAttrs := newState.Attributes | ||
for k, newV := range newAttrs { | ||
oldV, ok := oldAttrs[k] | ||
if !ok { | ||
continue // no old value to compare with | ||
} | ||
if newV == oldV { | ||
continue // no change to test | ||
} | ||
|
||
schemaList := addrToSchema(strings.Split(k, "."), m) | ||
if len(schemaList) == 0 { | ||
continue // no schema? weird, but not our responsibility to handle | ||
} | ||
schema := schemaList[len(schemaList)-1] | ||
if !schema.DiffSuppressOnRefresh || schema.DiffSuppressFunc == nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: why isn't the branching There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nevermind, I think I can answer my own question: this is to ensure this logic is applied selectively to sub-attributes of a schema. Even if most of it doesn't use it, if there is even just 1 attribute that does, we want to check it for each. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, unfortunately I think we do need to visit everything here to make sure none of them are in need of this work. An additional resource-level opt-in could potentially mitigate that but it seems like an annoying and unprecedented extra step, and in practice most (though certainly not all) resource types have only tens of attributes and so I don't expect this to be prohibitively expensive in the common case. |
||
continue // not relevant | ||
} | ||
|
||
if d == nil { | ||
// We populate "d" only on demand, to avoid the cost for most | ||
// existing schemas where DiffSuppressOnRefresh won't be set. | ||
var err error | ||
d, err = m.Data(newState, nil) | ||
if err != nil { | ||
// Should not happen if we got far enough to be doing this | ||
// analysis, but if it does then we'll bail out. | ||
tfsdklog.Warn(ctx, fmt.Sprintf("schemaMap.handleDiffSuppressOnRefresh failed to construct ResourceData: %s", err)) | ||
return | ||
} | ||
} | ||
|
||
if schema.DiffSuppressFunc(k, oldV, newV, d) { | ||
tfsdklog.Debug(ctx, fmt.Sprintf("ignoring change of %q due to DiffSuppressFunc", k)) | ||
newState.Attributes[k] = oldV // keep the old value, then | ||
apparentlymart marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
} | ||
|
||
func (m schemaMap) validate( | ||
k string, | ||
schema *Schema, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks so much for adding this.