Structured Plan Renderer: Remove attributes that do not match the relevant attributes filter (#32509)

* remove attributes that do not match the relevant attributes filter

* fix formatting

* fix renderer function, don't drop irrelevant attributes just mark them as no-ops

* fix imports
This commit is contained in:
Liam Cervante
2023-01-16 15:18:38 +01:00
committed by GitHub
parent 4fd8322802
commit e015b15f12
22 changed files with 1248 additions and 246 deletions

View File

@@ -0,0 +1,201 @@
package attribute_path
import "encoding/json"
// Matcher provides an interface for stepping through changes following an
// attribute path.
//
// GetChildWithKey and GetChildWithIndex will check if any of the internal paths
// match the provided key or index, and return a new Matcher that will match
// that children or potentially it's children.
//
// The caller of the above functions is required to know whether the next value
// in the path is a list type or an object type and call the relevant function,
// otherwise these functions will crash/panic.
//
// The Matches function returns true if the paths you have traversed until now
// ends.
type Matcher interface {
// Matches returns true if we have reached the end of a path and found an
// exact match.
Matches() bool
// MatchesPartial returns true if the current attribute is part of a path
// but not necessarily at the end of the path.
MatchesPartial() bool
GetChildWithKey(key string) Matcher
GetChildWithIndex(index int) Matcher
}
// Parse accepts a json.RawMessage and outputs a formatted Matcher object.
//
// Parse expects the message to be a JSON array of JSON arrays containing
// strings and floats. This function happily accepts a null input representing
// none of the changes in this resource are causing a replacement. The propagate
// argument tells the matcher to propagate any matches to the matched attributes
// children.
//
// In general, this function is designed to accept messages that have been
// produced by the lossy cty.Paths conversion functions within the jsonplan
// package. There is nothing particularly special about that conversion process
// though, it just produces the nested JSON arrays described above.
func Parse(message json.RawMessage, propagate bool) Matcher {
matcher := &PathMatcher{
Propagate: propagate,
}
if message == nil {
return matcher
}
if err := json.Unmarshal(message, &matcher.Paths); err != nil {
panic("failed to unmarshal attribute paths: " + err.Error())
}
return matcher
}
// Empty returns an empty PathMatcher that will by default match nothing.
//
// We give direct access to the PathMatcher struct so a matcher can be built
// in parts with the Append and AppendSingle functions.
func Empty(propagate bool) *PathMatcher {
return &PathMatcher{
Propagate: propagate,
}
}
// Append accepts an existing PathMatcher and returns a new one that attaches
// all the paths from message with the existing paths.
//
// The new PathMatcher is created fresh, and the existing one is unchanged.
func Append(matcher *PathMatcher, message json.RawMessage) *PathMatcher {
var values [][]interface{}
if err := json.Unmarshal(message, &values); err != nil {
panic("failed to unmarshal attribute paths: " + err.Error())
}
return &PathMatcher{
Propagate: matcher.Propagate,
Paths: append(matcher.Paths, values...),
}
}
// AppendSingle accepts an existing PathMatcher and returns a new one that
// attaches the single path from message with the existing paths.
//
// The new PathMatcher is created fresh, and the existing one is unchanged.
func AppendSingle(matcher *PathMatcher, message json.RawMessage) *PathMatcher {
var values []interface{}
if err := json.Unmarshal(message, &values); err != nil {
panic("failed to unmarshal attribute paths: " + err.Error())
}
return &PathMatcher{
Propagate: matcher.Propagate,
Paths: append(matcher.Paths, values),
}
}
// PathMatcher contains a slice of paths that represent paths through the values
// to relevant/tracked attributes.
type PathMatcher struct {
// We represent our internal paths as a [][]interface{} as the cty.Paths
// conversion process is lossy. Since the type information is lost there
// is no (easy) way to reproduce the original cty.Paths object. Instead,
// we simply rely on the external callers to know the type information and
// call the correct GetChild function.
Paths [][]interface{}
// Propagate tells the matcher that it should propagate any matches it finds
// onto the children of that match.
Propagate bool
}
func (p *PathMatcher) Matches() bool {
for _, path := range p.Paths {
if len(path) == 0 {
return true
}
}
return false
}
func (p *PathMatcher) MatchesPartial() bool {
return len(p.Paths) > 0
}
func (p *PathMatcher) GetChildWithKey(key string) Matcher {
child := &PathMatcher{
Propagate: p.Propagate,
}
for _, path := range p.Paths {
if len(path) == 0 {
// This means that the current value matched, but not necessarily
// it's child.
if p.Propagate {
// If propagate is true, then our child match our matches
child.Paths = append(child.Paths, path)
}
// If not we would simply drop this path from our set of paths but
// either way we just continue.
continue
}
if path[0].(string) == key {
child.Paths = append(child.Paths, path[1:])
}
}
return child
}
func (p *PathMatcher) GetChildWithIndex(index int) Matcher {
child := &PathMatcher{
Propagate: p.Propagate,
}
for _, path := range p.Paths {
if len(path) == 0 {
// This means that the current value matched, but not necessarily
// it's child.
if p.Propagate {
// If propagate is true, then our child match our matches
child.Paths = append(child.Paths, path)
}
// If not we would simply drop this path from our set of paths but
// either way we just continue.
continue
}
if int(path[0].(float64)) == index {
child.Paths = append(child.Paths, path[1:])
}
}
return child
}
// AlwaysMatcher returns a matcher that will always match all paths.
func AlwaysMatcher() Matcher {
return &alwaysMatcher{}
}
type alwaysMatcher struct{}
func (a *alwaysMatcher) Matches() bool {
return true
}
func (a *alwaysMatcher) MatchesPartial() bool {
return true
}
func (a *alwaysMatcher) GetChildWithKey(_ string) Matcher {
return a
}
func (a *alwaysMatcher) GetChildWithIndex(_ int) Matcher {
return a
}

View File

@@ -0,0 +1,253 @@
package attribute_path
import "testing"
func TestPathMatcher_FollowsPath(t *testing.T) {
var matcher Matcher
matcher = &PathMatcher{
Paths: [][]interface{}{
{
float64(0),
"key",
float64(0),
},
},
}
if matcher.Matches() {
t.Errorf("should not have exact matched at base level")
}
if !matcher.MatchesPartial() {
t.Errorf("should have partial matched at base level")
}
matcher = matcher.GetChildWithIndex(0)
if matcher.Matches() {
t.Errorf("should not have exact matched at first level")
}
if !matcher.MatchesPartial() {
t.Errorf("should have partial matched at first level")
}
matcher = matcher.GetChildWithKey("key")
if matcher.Matches() {
t.Errorf("should not have exact matched at second level")
}
if !matcher.MatchesPartial() {
t.Errorf("should have partial matched at second level")
}
matcher = matcher.GetChildWithIndex(0)
if !matcher.Matches() {
t.Errorf("should have exact matched at leaf level")
}
if !matcher.MatchesPartial() {
t.Errorf("should have partial matched at leaf level")
}
}
func TestPathMatcher_Propagates(t *testing.T) {
var matcher Matcher
matcher = &PathMatcher{
Paths: [][]interface{}{
{
float64(0),
"key",
},
},
Propagate: true,
}
if matcher.Matches() {
t.Errorf("should not have exact matched at base level")
}
if !matcher.MatchesPartial() {
t.Errorf("should have partial matched at base level")
}
matcher = matcher.GetChildWithIndex(0)
if matcher.Matches() {
t.Errorf("should not have exact matched at first level")
}
if !matcher.MatchesPartial() {
t.Errorf("should have partial matched at first level")
}
matcher = matcher.GetChildWithKey("key")
if !matcher.Matches() {
t.Errorf("should have exact matched at second level")
}
if !matcher.MatchesPartial() {
t.Errorf("should have partial matched at second level")
}
matcher = matcher.GetChildWithIndex(0)
if !matcher.Matches() {
t.Errorf("should have exact matched at leaf level")
}
if !matcher.MatchesPartial() {
t.Errorf("should have partial matched at leaf level")
}
}
func TestPathMatcher_DoesNotPropagate(t *testing.T) {
var matcher Matcher
matcher = &PathMatcher{
Paths: [][]interface{}{
{
float64(0),
"key",
},
},
}
if matcher.Matches() {
t.Errorf("should not have exact matched at base level")
}
if !matcher.MatchesPartial() {
t.Errorf("should have partial matched at base level")
}
matcher = matcher.GetChildWithIndex(0)
if matcher.Matches() {
t.Errorf("should not have exact matched at first level")
}
if !matcher.MatchesPartial() {
t.Errorf("should have partial matched at first level")
}
matcher = matcher.GetChildWithKey("key")
if !matcher.Matches() {
t.Errorf("should have exact matched at second level")
}
if !matcher.MatchesPartial() {
t.Errorf("should have partial matched at second level")
}
matcher = matcher.GetChildWithIndex(0)
if matcher.Matches() {
t.Errorf("should not have exact matched at leaf level")
}
if matcher.MatchesPartial() {
t.Errorf("should not have partial matched at leaf level")
}
}
func TestPathMatcher_BreaksPath(t *testing.T) {
var matcher Matcher
matcher = &PathMatcher{
Paths: [][]interface{}{
{
float64(0),
"key",
float64(0),
},
},
}
if matcher.Matches() {
t.Errorf("should not have exact matched at base level")
}
if !matcher.MatchesPartial() {
t.Errorf("should have partial matched at base level")
}
matcher = matcher.GetChildWithIndex(0)
if matcher.Matches() {
t.Errorf("should not have exact matched at first level")
}
if !matcher.MatchesPartial() {
t.Errorf("should have partial matched at first level")
}
matcher = matcher.GetChildWithKey("invalid")
if matcher.Matches() {
t.Errorf("should not have exact matched at second level")
}
if matcher.MatchesPartial() {
t.Errorf("should not have partial matched at second level")
}
}
func TestPathMatcher_MultiplePaths(t *testing.T) {
var matcher Matcher
matcher = &PathMatcher{
Paths: [][]interface{}{
{
float64(0),
"key",
float64(0),
},
{
float64(0),
"key",
float64(1),
},
},
}
if matcher.Matches() {
t.Errorf("should not have exact matched at base level")
}
if !matcher.MatchesPartial() {
t.Errorf("should have partial matched at base level")
}
matcher = matcher.GetChildWithIndex(0)
if matcher.Matches() {
t.Errorf("should not have exact matched at first level")
}
if !matcher.MatchesPartial() {
t.Errorf("should have partial matched at first level")
}
matcher = matcher.GetChildWithKey("key")
if matcher.Matches() {
t.Errorf("should not have exact matched at second level")
}
if !matcher.MatchesPartial() {
t.Errorf("should have partial matched at second level")
}
validZero := matcher.GetChildWithIndex(0)
validOne := matcher.GetChildWithIndex(1)
invalid := matcher.GetChildWithIndex(2)
if !validZero.Matches() {
t.Errorf("should have exact matched at leaf level")
}
if !validZero.MatchesPartial() {
t.Errorf("should have partial matched at leaf level")
}
if !validOne.Matches() {
t.Errorf("should have exact matched at leaf level")
}
if !validOne.MatchesPartial() {
t.Errorf("should have partial matched at leaf level")
}
if invalid.Matches() {
t.Errorf("should not have exact matched at leaf level")
}
if invalid.MatchesPartial() {
t.Errorf("should not have partial matched at leaf level")
}
}

View File

@@ -25,6 +25,11 @@ func (change Change) ComputeDiffForBlock(block *jsonprovider.Block) computed.Dif
for key, attr := range block.Attributes {
childValue := blockValue.getChild(key)
if !childValue.RelevantAttributes.MatchesPartial() {
// Mark non-relevant attributes as unchanged.
childValue = childValue.AsNoOp()
}
// Empty strings in blocks should be considered null for legacy reasons.
// The SDK doesn't support null strings yet, so we work around this now.
if before, ok := childValue.Before.(string); ok && len(before) == 0 {
@@ -61,9 +66,14 @@ func (change Change) ComputeDiffForBlock(block *jsonprovider.Block) computed.Dif
for key, blockType := range block.BlockTypes {
childValue := blockValue.getChild(key)
if !childValue.RelevantAttributes.MatchesPartial() {
// Mark non-relevant attributes as unchanged.
childValue = childValue.AsNoOp()
}
beforeSensitive := childValue.isBeforeSensitive()
afterSensitive := childValue.isAfterSensitive()
forcesReplacement := childValue.ReplacePaths.ForcesReplacement()
forcesReplacement := childValue.ReplacePaths.Matches()
switch NestingMode(blockType.NestingMode) {
case nestingModeSet:
@@ -103,5 +113,5 @@ func (change Change) ComputeDiffForBlock(block *jsonprovider.Block) computed.Dif
}
}
return computed.NewDiff(renderers.Block(attributes, blocks), current, change.ReplacePaths.ForcesReplacement())
return computed.NewDiff(renderers.Block(attributes, blocks), current, change.ReplacePaths.Matches())
}

View File

@@ -5,7 +5,7 @@ import (
"reflect"
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
"github.com/hashicorp/terraform/internal/command/jsonformat/differ/replace"
"github.com/hashicorp/terraform/internal/command/jsonformat/differ/attribute_path"
"github.com/hashicorp/terraform/internal/command/jsonplan"
"github.com/hashicorp/terraform/internal/plans"
)
@@ -76,27 +76,33 @@ type Change struct {
// sensitive.
AfterSensitive interface{}
// ReplacePaths generally contains nested slices that describe paths to
// elements or attributes that are causing the overall resource to be
// replaced.
ReplacePaths replace.ForcesReplacement
// ReplacePaths contains a set of paths that point to attributes/elements
// that are causing the overall resource to be replaced rather than simply
// updated.
ReplacePaths attribute_path.Matcher
// RelevantAttributes contains a set of paths that point attributes/elements
// that we should display. Any element/attribute not matched by this Matcher
// should be skipped.
RelevantAttributes attribute_path.Matcher
}
// FromJsonChange unmarshals the raw []byte values in the jsonplan.Change
// structs into generic interface{} types that can be reasoned about.
func FromJsonChange(change jsonplan.Change) Change {
func FromJsonChange(change jsonplan.Change, relevantAttributes attribute_path.Matcher) Change {
return Change{
Before: unmarshalGeneric(change.Before),
After: unmarshalGeneric(change.After),
Unknown: unmarshalGeneric(change.AfterUnknown),
BeforeSensitive: unmarshalGeneric(change.BeforeSensitive),
AfterSensitive: unmarshalGeneric(change.AfterSensitive),
ReplacePaths: replace.Parse(change.ReplacePaths),
Before: unmarshalGeneric(change.Before),
After: unmarshalGeneric(change.After),
Unknown: unmarshalGeneric(change.AfterUnknown),
BeforeSensitive: unmarshalGeneric(change.BeforeSensitive),
AfterSensitive: unmarshalGeneric(change.AfterSensitive),
ReplacePaths: attribute_path.Parse(change.ReplacePaths, false),
RelevantAttributes: relevantAttributes,
}
}
func (change Change) asDiff(renderer computed.DiffRenderer) computed.Diff {
return computed.NewDiff(renderer, change.calculateChange(), change.ReplacePaths.ForcesReplacement())
return computed.NewDiff(renderer, change.calculateChange(), change.ReplacePaths.Matches())
}
func (change Change) calculateChange() plans.Action {
@@ -138,6 +144,23 @@ func (change Change) getDefaultActionForIteration() plans.Action {
return plans.NoOp
}
// AsNoOp returns the current change as if it is a NoOp operation.
//
// Basically it replaces all the after values with the before values.
func (change Change) AsNoOp() Change {
return Change{
BeforeExplicit: change.BeforeExplicit,
AfterExplicit: change.BeforeExplicit,
Before: change.Before,
After: change.Before,
Unknown: false,
BeforeSensitive: change.BeforeSensitive,
AfterSensitive: change.BeforeSensitive,
ReplacePaths: change.ReplacePaths,
RelevantAttributes: change.RelevantAttributes,
}
}
func unmarshalGeneric(raw json.RawMessage) interface{} {
if raw == nil {
return nil

View File

@@ -1,6 +1,8 @@
package differ
import "github.com/hashicorp/terraform/internal/command/jsonformat/differ/replace"
import (
"github.com/hashicorp/terraform/internal/command/jsonformat/differ/attribute_path"
)
// ChangeMap is a Change that represents a Map or an Object type, and has
// converted the relevant interfaces into maps for easier access.
@@ -24,17 +26,21 @@ type ChangeMap struct {
AfterSensitive map[string]interface{}
// ReplacePaths matches the same attributes in Change exactly.
ReplacePaths replace.ForcesReplacement
ReplacePaths attribute_path.Matcher
// RelevantAttributes matches the same attributes in Change exactly.
RelevantAttributes attribute_path.Matcher
}
func (change Change) asMap() ChangeMap {
return ChangeMap{
Before: genericToMap(change.Before),
After: genericToMap(change.After),
Unknown: genericToMap(change.Unknown),
BeforeSensitive: genericToMap(change.BeforeSensitive),
AfterSensitive: genericToMap(change.AfterSensitive),
ReplacePaths: change.ReplacePaths,
Before: genericToMap(change.Before),
After: genericToMap(change.After),
Unknown: genericToMap(change.Unknown),
BeforeSensitive: genericToMap(change.BeforeSensitive),
AfterSensitive: genericToMap(change.AfterSensitive),
ReplacePaths: change.ReplacePaths,
RelevantAttributes: change.RelevantAttributes,
}
}
@@ -46,14 +52,15 @@ func (m ChangeMap) getChild(key string) Change {
afterSensitive, _ := getFromGenericMap(m.AfterSensitive, key)
return Change{
BeforeExplicit: beforeExplicit,
AfterExplicit: afterExplicit,
Before: before,
After: after,
Unknown: unknown,
BeforeSensitive: beforeSensitive,
AfterSensitive: afterSensitive,
ReplacePaths: m.ReplacePaths.GetChildWithKey(key),
BeforeExplicit: beforeExplicit,
AfterExplicit: afterExplicit,
Before: before,
After: after,
Unknown: unknown,
BeforeSensitive: beforeSensitive,
AfterSensitive: afterSensitive,
ReplacePaths: m.ReplacePaths.GetChildWithKey(key),
RelevantAttributes: m.RelevantAttributes.GetChildWithKey(key),
}
}

View File

@@ -1,6 +1,8 @@
package differ
import "github.com/hashicorp/terraform/internal/command/jsonformat/differ/replace"
import (
"github.com/hashicorp/terraform/internal/command/jsonformat/differ/attribute_path"
)
// ChangeSlice is a Change that represents a Tuple, Set, or List type, and has
// converted the relevant interfaces into slices for easier access.
@@ -23,17 +25,21 @@ type ChangeSlice struct {
AfterSensitive []interface{}
// ReplacePaths matches the same attributes in Change exactly.
ReplacePaths replace.ForcesReplacement
ReplacePaths attribute_path.Matcher
// RelevantAttributes matches the same attributes in Change exactly.
RelevantAttributes attribute_path.Matcher
}
func (change Change) asSlice() ChangeSlice {
return ChangeSlice{
Before: genericToSlice(change.Before),
After: genericToSlice(change.After),
Unknown: genericToSlice(change.Unknown),
BeforeSensitive: genericToSlice(change.BeforeSensitive),
AfterSensitive: genericToSlice(change.AfterSensitive),
ReplacePaths: change.ReplacePaths,
Before: genericToSlice(change.Before),
After: genericToSlice(change.After),
Unknown: genericToSlice(change.Unknown),
BeforeSensitive: genericToSlice(change.BeforeSensitive),
AfterSensitive: genericToSlice(change.AfterSensitive),
ReplacePaths: change.ReplacePaths,
RelevantAttributes: change.RelevantAttributes,
}
}
@@ -44,15 +50,21 @@ func (s ChangeSlice) getChild(beforeIx, afterIx int) Change {
beforeSensitive, _ := getFromGenericSlice(s.BeforeSensitive, beforeIx)
afterSensitive, _ := getFromGenericSlice(s.AfterSensitive, afterIx)
mostRelevantIx := beforeIx
if beforeIx < 0 || beforeIx >= len(s.Before) {
mostRelevantIx = afterIx
}
return Change{
BeforeExplicit: beforeExplicit,
AfterExplicit: afterExplicit,
Before: before,
After: after,
Unknown: unknown,
BeforeSensitive: beforeSensitive,
AfterSensitive: afterSensitive,
ReplacePaths: s.ReplacePaths.GetChildWithIndex(beforeIx),
BeforeExplicit: beforeExplicit,
AfterExplicit: afterExplicit,
Before: before,
After: after,
Unknown: unknown,
BeforeSensitive: beforeSensitive,
AfterSensitive: afterSensitive,
ReplacePaths: s.ReplacePaths.GetChildWithIndex(mostRelevantIx),
RelevantAttributes: s.RelevantAttributes.GetChildWithIndex(mostRelevantIx),
}
}

View File

@@ -9,7 +9,7 @@ import (
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers"
"github.com/hashicorp/terraform/internal/command/jsonformat/differ/replace"
"github.com/hashicorp/terraform/internal/command/jsonformat/differ/attribute_path"
"github.com/hashicorp/terraform/internal/command/jsonprovider"
"github.com/hashicorp/terraform/internal/plans"
)
@@ -48,6 +48,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
validateObject renderers.ValidateDiffFunction
validateNestedObject renderers.ValidateDiffFunction
validateDiffs map[string]renderers.ValidateDiffFunction
validateList renderers.ValidateDiffFunction
validateReplace bool
validateAction plans.Action
// Sets break changes out differently to the other collections, so they
@@ -510,8 +511,8 @@ func TestValue_ObjectAttributes(t *testing.T) {
After: map[string]interface{}{
"attribute_one": "new",
},
ReplacePaths: replace.ForcesReplacement{
ReplacePaths: [][]interface{}{
ReplacePaths: &attribute_path.PathMatcher{
Paths: [][]interface{}{
{},
},
},
@@ -537,7 +538,7 @@ func TestValue_ObjectAttributes(t *testing.T) {
"attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false),
},
Action: plans.Create,
Replace: false,
Replace: true,
},
},
},
@@ -549,8 +550,8 @@ func TestValue_ObjectAttributes(t *testing.T) {
After: map[string]interface{}{
"attribute_one": "new",
},
ReplacePaths: replace.ForcesReplacement{
ReplacePaths: [][]interface{}{
ReplacePaths: &attribute_path.PathMatcher{
Paths: [][]interface{}{
{"attribute_one"},
},
},
@@ -573,7 +574,62 @@ func TestValue_ObjectAttributes(t *testing.T) {
},
After: SetDiffEntry{
ObjectDiff: map[string]renderers.ValidateDiffFunction{
"attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false),
"attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, true),
},
Action: plans.Create,
Replace: false,
},
},
},
"update_includes_relevant_attributes": {
input: Change{
Before: map[string]interface{}{
"attribute_one": "old_one",
"attribute_two": "old_two",
},
After: map[string]interface{}{
"attribute_one": "new_one",
"attribute_two": "new_two",
},
RelevantAttributes: &attribute_path.PathMatcher{
Paths: [][]interface{}{
{"attribute_one"},
},
},
},
attributes: map[string]cty.Type{
"attribute_one": cty.String,
"attribute_two": cty.String,
},
validateDiffs: map[string]renderers.ValidateDiffFunction{
"attribute_one": renderers.ValidatePrimitive("old_one", "new_one", plans.Update, false),
"attribute_two": renderers.ValidatePrimitive("old_two", "old_two", plans.NoOp, false),
},
validateList: renderers.ValidateList([]renderers.ValidateDiffFunction{
renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{
// Lists are a bit special, and in this case is actually
// going to ignore the relevant attributes. This is
// deliberate. See the comments in list.go for an
// explanation.
"attribute_one": renderers.ValidatePrimitive("old_one", "new_one", plans.Update, false),
"attribute_two": renderers.ValidatePrimitive("old_two", "new_two", plans.Update, false),
}, plans.Update, false),
}, plans.Update, false),
validateAction: plans.Update,
validateReplace: false,
validateSetDiffs: &SetDiff{
Before: SetDiffEntry{
ObjectDiff: map[string]renderers.ValidateDiffFunction{
"attribute_one": renderers.ValidatePrimitive("old_one", nil, plans.Delete, false),
"attribute_two": renderers.ValidatePrimitive("old_two", nil, plans.Delete, false),
},
Action: plans.Delete,
Replace: false,
},
After: SetDiffEntry{
ObjectDiff: map[string]renderers.ValidateDiffFunction{
"attribute_one": renderers.ValidatePrimitive(nil, "new_one", plans.Create, false),
"attribute_two": renderers.ValidatePrimitive(nil, "new_two", plans.Create, false),
},
Action: plans.Create,
Replace: false,
@@ -585,6 +641,14 @@ func TestValue_ObjectAttributes(t *testing.T) {
for name, tmp := range tcs {
tc := tmp
// Let's set some default values on the input.
if tc.input.RelevantAttributes == nil {
tc.input.RelevantAttributes = attribute_path.AlwaysMatcher()
}
if tc.input.ReplacePaths == nil {
tc.input.ReplacePaths = &attribute_path.PathMatcher{}
}
collectionDefaultAction := plans.Update
if name == "ignores_unset_fields" {
// Special case for this test, as it is the only one that doesn't
@@ -648,6 +712,11 @@ func TestValue_ObjectAttributes(t *testing.T) {
input := wrapChangeInSlice(tc.input)
if tc.validateList != nil {
tc.validateList(t, input.ComputeDiffForAttribute(attribute))
return
}
if tc.validateObject != nil {
validate := renderers.ValidateList([]renderers.ValidateDiffFunction{
tc.validateObject,
@@ -1084,6 +1153,8 @@ func TestValue_BlockAttributesAndNestedBlocks(t *testing.T) {
After: map[string]interface{}{
"block_type": tc.after,
},
ReplacePaths: &attribute_path.PathMatcher{},
RelevantAttributes: attribute_path.AlwaysMatcher(),
}
block := &jsonprovider.Block{
@@ -1112,6 +1183,8 @@ func TestValue_BlockAttributesAndNestedBlocks(t *testing.T) {
"one": tc.after,
},
},
ReplacePaths: &attribute_path.PathMatcher{},
RelevantAttributes: attribute_path.AlwaysMatcher(),
}
block := &jsonprovider.Block{
@@ -1142,6 +1215,8 @@ func TestValue_BlockAttributesAndNestedBlocks(t *testing.T) {
tc.after,
},
},
ReplacePaths: &attribute_path.PathMatcher{},
RelevantAttributes: attribute_path.AlwaysMatcher(),
}
block := &jsonprovider.Block{
@@ -1172,6 +1247,8 @@ func TestValue_BlockAttributesAndNestedBlocks(t *testing.T) {
tc.after,
},
},
ReplacePaths: &attribute_path.PathMatcher{},
RelevantAttributes: attribute_path.AlwaysMatcher(),
}
block := &jsonprovider.Block{
@@ -1416,6 +1493,15 @@ func TestValue_Outputs(t *testing.T) {
}
for name, tc := range tcs {
// Let's set some default values on the input.
if tc.input.RelevantAttributes == nil {
tc.input.RelevantAttributes = attribute_path.AlwaysMatcher()
}
if tc.input.ReplacePaths == nil {
tc.input.ReplacePaths = &attribute_path.PathMatcher{}
}
t.Run(name, func(t *testing.T) {
tc.validateDiff(t, tc.input.ComputeDiffForOutput())
})
@@ -1543,8 +1629,8 @@ func TestValue_PrimitiveAttributes(t *testing.T) {
input: Change{
Before: "old",
After: "new",
ReplacePaths: replace.ForcesReplacement{
ReplacePaths: [][]interface{}{
ReplacePaths: &attribute_path.PathMatcher{
Paths: [][]interface{}{
{}, // An empty path suggests replace should be true.
},
},
@@ -1553,7 +1639,7 @@ func TestValue_PrimitiveAttributes(t *testing.T) {
validateDiff: renderers.ValidatePrimitive("old", "new", plans.Update, true),
validateSliceDiffs: []renderers.ValidateDiffFunction{
renderers.ValidatePrimitive("old", nil, plans.Delete, true),
renderers.ValidatePrimitive(nil, "new", plans.Create, false),
renderers.ValidatePrimitive(nil, "new", plans.Create, true),
},
},
"noop": {
@@ -1595,6 +1681,14 @@ func TestValue_PrimitiveAttributes(t *testing.T) {
for name, tmp := range tcs {
tc := tmp
// Let's set some default values on the input.
if tc.input.RelevantAttributes == nil {
tc.input.RelevantAttributes = attribute_path.AlwaysMatcher()
}
if tc.input.ReplacePaths == nil {
tc.input.ReplacePaths = &attribute_path.PathMatcher{}
}
defaultCollectionsAction := plans.Update
if name == "noop" {
defaultCollectionsAction = plans.NoOp
@@ -2014,12 +2108,373 @@ func TestValue_CollectionAttributes(t *testing.T) {
}
for name, tc := range tcs {
// Let's set some default values on the input.
if tc.input.RelevantAttributes == nil {
tc.input.RelevantAttributes = attribute_path.AlwaysMatcher()
}
if tc.input.ReplacePaths == nil {
tc.input.ReplacePaths = &attribute_path.PathMatcher{}
}
t.Run(name, func(t *testing.T) {
tc.validateDiff(t, tc.input.ComputeDiffForAttribute(tc.attribute))
})
}
}
func TestRelevantAttributes(t *testing.T) {
tcs := map[string]struct {
input Change
block *jsonprovider.Block
validate renderers.ValidateDiffFunction
}{
"simple_attributes": {
input: Change{
Before: map[string]interface{}{
"id": "old_id",
"ignore": "doesn't matter",
},
After: map[string]interface{}{
"id": "new_id",
"ignore": "doesn't matter but modified",
},
RelevantAttributes: &attribute_path.PathMatcher{
Paths: [][]interface{}{
{
"id",
},
},
},
},
block: &jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"id": {
AttributeType: unmarshalType(t, cty.String),
},
"ignore": {
AttributeType: unmarshalType(t, cty.String),
},
},
},
validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{
"id": renderers.ValidatePrimitive("old_id", "new_id", plans.Update, false),
"ignore": renderers.ValidatePrimitive("doesn't matter", "doesn't matter", plans.NoOp, false),
}, nil, nil, nil, nil, plans.Update, false),
},
"nested_attributes": {
input: Change{
Before: map[string]interface{}{
"list_block": []interface{}{
map[string]interface{}{
"id": "old_one",
},
map[string]interface{}{
"id": "ignored",
},
},
},
After: map[string]interface{}{
"list_block": []interface{}{
map[string]interface{}{
"id": "new_one",
},
map[string]interface{}{
"id": "ignored_but_changed",
},
},
},
RelevantAttributes: &attribute_path.PathMatcher{
Paths: [][]interface{}{
{
"list_block",
float64(0),
"id",
},
},
},
},
block: &jsonprovider.Block{
BlockTypes: map[string]*jsonprovider.BlockType{
"list_block": {
Block: &jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"id": {
AttributeType: unmarshalType(t, cty.String),
},
},
},
NestingMode: "list",
},
},
},
validate: renderers.ValidateBlock(nil, nil, map[string][]renderers.ValidateDiffFunction{
"list_block": {
renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{
"id": renderers.ValidatePrimitive("old_one", "new_one", plans.Update, false),
}, nil, nil, nil, nil, plans.Update, false),
renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{
"id": renderers.ValidatePrimitive("ignored", "ignored", plans.NoOp, false),
}, nil, nil, nil, nil, plans.NoOp, false),
},
}, nil, nil, plans.Update, false),
},
"nested_attributes_in_object": {
input: Change{
Before: map[string]interface{}{
"object": map[string]interface{}{
"id": "old_id",
},
},
After: map[string]interface{}{
"object": map[string]interface{}{
"id": "new_id",
},
},
RelevantAttributes: &attribute_path.PathMatcher{
Propagate: true,
Paths: [][]interface{}{
{
"object", // Even though we just specify object, it should now include every below object as well.
},
},
},
},
block: &jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"object": {
AttributeType: unmarshalType(t, cty.Object(map[string]cty.Type{
"id": cty.String,
})),
},
},
},
validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{
"object": renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{
"id": renderers.ValidatePrimitive("old_id", "new_id", plans.Update, false),
}, plans.Update, false),
}, nil, nil, nil, nil, plans.Update, false),
},
"elements_in_list": {
input: Change{
Before: map[string]interface{}{
"list": []interface{}{
0, 1, 2, 3, 4,
},
},
After: map[string]interface{}{
"list": []interface{}{
0, 5, 6, 7, 4,
},
},
RelevantAttributes: &attribute_path.PathMatcher{
Paths: [][]interface{}{ // The list is actually just going to ignore this.
{
"list",
float64(0),
},
{
"list",
float64(2),
},
{
"list",
float64(4),
},
},
},
},
block: &jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"list": {
AttributeType: unmarshalType(t, cty.List(cty.Number)),
},
},
},
validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{
// The list validator below just ignores our relevant
// attributes. This is deliberate.
"list": renderers.ValidateList([]renderers.ValidateDiffFunction{
renderers.ValidatePrimitive(0, 0, plans.NoOp, false),
renderers.ValidatePrimitive(1, nil, plans.Delete, false),
renderers.ValidatePrimitive(2, nil, plans.Delete, false),
renderers.ValidatePrimitive(3, nil, plans.Delete, false),
renderers.ValidatePrimitive(nil, 5, plans.Create, false),
renderers.ValidatePrimitive(nil, 6, plans.Create, false),
renderers.ValidatePrimitive(nil, 7, plans.Create, false),
renderers.ValidatePrimitive(4, 4, plans.NoOp, false),
}, plans.Update, false),
}, nil, nil, nil, nil, plans.Update, false),
},
"elements_in_map": {
input: Change{
Before: map[string]interface{}{
"map": map[string]interface{}{
"key_one": "value_one",
"key_two": "value_two",
"key_three": "value_three",
},
},
After: map[string]interface{}{
"map": map[string]interface{}{
"key_one": "value_three",
"key_two": "value_seven",
"key_four": "value_four",
},
},
RelevantAttributes: &attribute_path.PathMatcher{
Paths: [][]interface{}{
{
"map",
"key_one",
},
{
"map",
"key_three",
},
{
"map",
"key_four",
},
},
},
},
block: &jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"map": {
AttributeType: unmarshalType(t, cty.Map(cty.String)),
},
},
},
validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{
"map": renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{
"key_one": renderers.ValidatePrimitive("value_one", "value_three", plans.Update, false),
"key_two": renderers.ValidatePrimitive("value_two", "value_two", plans.NoOp, false),
"key_three": renderers.ValidatePrimitive("value_three", nil, plans.Delete, false),
"key_four": renderers.ValidatePrimitive(nil, "value_four", plans.Create, false),
}, plans.Update, false),
}, nil, nil, nil, nil, plans.Update, false),
},
"elements_in_set": {
input: Change{
Before: map[string]interface{}{
"set": []interface{}{
0, 1, 2, 3, 4,
},
},
After: map[string]interface{}{
"set": []interface{}{
0, 2, 4, 5, 6,
},
},
RelevantAttributes: &attribute_path.PathMatcher{
Propagate: true,
Paths: [][]interface{}{
{
"set",
},
},
},
},
block: &jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"set": {
AttributeType: unmarshalType(t, cty.Set(cty.Number)),
},
},
},
validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{
"set": renderers.ValidateSet([]renderers.ValidateDiffFunction{
renderers.ValidatePrimitive(0, 0, plans.NoOp, false),
renderers.ValidatePrimitive(1, nil, plans.Delete, false),
renderers.ValidatePrimitive(2, 2, plans.NoOp, false),
renderers.ValidatePrimitive(3, nil, plans.Delete, false),
renderers.ValidatePrimitive(4, 4, plans.NoOp, false),
renderers.ValidatePrimitive(nil, 5, plans.Create, false),
renderers.ValidatePrimitive(nil, 6, plans.Create, false),
}, plans.Update, false),
}, nil, nil, nil, nil, plans.Update, false),
},
"dynamic_types": {
input: Change{
Before: map[string]interface{}{
"dynamic_nested_type": map[string]interface{}{
"nested_id": "nomatch",
"nested_object": map[string]interface{}{
"nested_nested_id": "matched",
},
},
"dynamic_nested_type_match": map[string]interface{}{
"nested_id": "allmatch",
"nested_object": map[string]interface{}{
"nested_nested_id": "allmatch",
},
},
},
After: map[string]interface{}{
"dynamic_nested_type": map[string]interface{}{
"nested_id": "nomatch_changed",
"nested_object": map[string]interface{}{
"nested_nested_id": "matched",
},
},
"dynamic_nested_type_match": map[string]interface{}{
"nested_id": "allmatch",
"nested_object": map[string]interface{}{
"nested_nested_id": "allmatch",
},
},
},
RelevantAttributes: &attribute_path.PathMatcher{
Propagate: true,
Paths: [][]interface{}{
{
"dynamic_nested_type",
"nested_object",
"nested_nested_id",
},
{
"dynamic_nested_type_match",
},
},
},
},
block: &jsonprovider.Block{
Attributes: map[string]*jsonprovider.Attribute{
"dynamic_nested_type": {
AttributeType: unmarshalType(t, cty.DynamicPseudoType),
},
"dynamic_nested_type_match": {
AttributeType: unmarshalType(t, cty.DynamicPseudoType),
},
},
},
validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{
"dynamic_nested_type": renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{
"nested_id": renderers.ValidatePrimitive("nomatch", "nomatch", plans.NoOp, false),
"nested_object": renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{
"nested_nested_id": renderers.ValidatePrimitive("matched", "matched", plans.NoOp, false),
}, plans.NoOp, false),
}, plans.NoOp, false),
"dynamic_nested_type_match": renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{
"nested_id": renderers.ValidatePrimitive("allmatch", "allmatch", plans.NoOp, false),
"nested_object": renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{
"nested_nested_id": renderers.ValidatePrimitive("allmatch", "allmatch", plans.NoOp, false),
}, plans.NoOp, false),
}, plans.NoOp, false),
}, nil, nil, nil, nil, plans.NoOp, false),
},
}
for name, tc := range tcs {
if tc.input.ReplacePaths == nil {
tc.input.ReplacePaths = &attribute_path.PathMatcher{}
}
t.Run(name, func(t *testing.T) {
tc.validate(t, tc.input.ComputeDiffForBlock(tc.block))
})
}
}
// unmarshalType converts a cty.Type into a json.RawMessage understood by the
// schema. It also lets the testing framework handle any errors to keep the API
// clean.
@@ -2070,20 +2525,37 @@ func wrapChangeInMap(input Change) Change {
func wrapChange(input Change, step interface{}, wrap func(interface{}, interface{}, bool) interface{}) Change {
replacePaths := replace.ForcesReplacement{}
for _, path := range input.ReplacePaths.ReplacePaths {
replacePaths := &attribute_path.PathMatcher{}
for _, path := range input.ReplacePaths.(*attribute_path.PathMatcher).Paths {
var updated []interface{}
updated = append(updated, step)
updated = append(updated, path...)
replacePaths.ReplacePaths = append(replacePaths.ReplacePaths, updated)
replacePaths.Paths = append(replacePaths.Paths, updated)
}
// relevantAttributes usually default to AlwaysMatcher, which means we can
// just ignore it. But if we have had some paths specified we need to wrap
// those as well.
relevantAttributes := input.RelevantAttributes
if concrete, ok := relevantAttributes.(*attribute_path.PathMatcher); ok {
newRelevantAttributes := &attribute_path.PathMatcher{}
for _, path := range concrete.Paths {
var updated []interface{}
updated = append(updated, step)
updated = append(updated, path...)
newRelevantAttributes.Paths = append(newRelevantAttributes.Paths, updated)
}
relevantAttributes = newRelevantAttributes
}
return Change{
Before: wrap(input.Before, nil, input.BeforeExplicit),
After: wrap(input.After, input.Unknown, input.AfterExplicit),
Unknown: wrap(input.Unknown, nil, false),
BeforeSensitive: wrap(input.BeforeSensitive, nil, false),
AfterSensitive: wrap(input.AfterSensitive, nil, false),
ReplacePaths: replacePaths,
Before: wrap(input.Before, nil, input.BeforeExplicit),
After: wrap(input.After, input.Unknown, input.AfterExplicit),
Unknown: wrap(input.Unknown, nil, false),
BeforeSensitive: wrap(input.BeforeSensitive, nil, false),
AfterSensitive: wrap(input.AfterSensitive, nil, false),
ReplacePaths: replacePaths,
RelevantAttributes: relevantAttributes,
}
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/hashicorp/terraform/internal/command/jsonformat/collections"
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
"github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers"
"github.com/hashicorp/terraform/internal/command/jsonformat/differ/attribute_path"
"github.com/hashicorp/terraform/internal/command/jsonprovider"
"github.com/hashicorp/terraform/internal/plans"
)
@@ -14,7 +15,29 @@ func (change Change) computeAttributeDiffAsList(elementType cty.Type) computed.D
sliceValue := change.asSlice()
processIndices := func(beforeIx, afterIx int) computed.Diff {
return sliceValue.getChild(beforeIx, afterIx).computeDiffForType(elementType)
value := sliceValue.getChild(beforeIx, afterIx)
// It's actually really difficult to render the diffs when some indices
// within a slice are relevant and others aren't. To make this simpler
// we just treat all children of a relevant list or set as also
// relevant.
//
// Interestingly the terraform plan builder also agrees with this, and
// never sets relevant attributes beneath lists or sets. We're just
// going to enforce this logic here as well. If the collection is
// relevant (decided elsewhere), then every element in the collection is
// also relevant. To be clear, in practice even if we didn't do the
// following explicitly the effect would be the same. It's just nicer
// for us to be clear about the behaviour we expect.
//
// What makes this difficult is the fact that the beforeIx and afterIx
// can be different, and it's quite difficult to work out which one is
// the relevant one. For nested lists, block lists, and tuples it's much
// easier because we always process the same indices in the before and
// after.
value.RelevantAttributes = attribute_path.AlwaysMatcher()
return value.computeDiffForType(elementType)
}
isObjType := func(_ interface{}) bool {
@@ -22,7 +45,7 @@ func (change Change) computeAttributeDiffAsList(elementType cty.Type) computed.D
}
elements, current := collections.TransformSlice(sliceValue.Before, sliceValue.After, processIndices, isObjType)
return computed.NewDiff(renderers.List(elements), current, change.ReplacePaths.ForcesReplacement())
return computed.NewDiff(renderers.List(elements), current, change.ReplacePaths.Matches())
}
func (change Change) computeAttributeDiffAsNestedList(attributes map[string]*jsonprovider.Attribute) computed.Diff {
@@ -36,7 +59,7 @@ func (change Change) computeAttributeDiffAsNestedList(attributes map[string]*jso
elements = append(elements, element)
current = collections.CompareActions(current, element.Action)
})
return computed.NewDiff(renderers.NestedList(elements), current, change.ReplacePaths.ForcesReplacement())
return computed.NewDiff(renderers.NestedList(elements), current, change.ReplacePaths.Matches())
}
func (change Change) computeBlockDiffsAsList(block *jsonprovider.Block) ([]computed.Diff, plans.Action) {
@@ -53,6 +76,11 @@ func (change Change) computeBlockDiffsAsList(block *jsonprovider.Block) ([]compu
func (change Change) processNestedList(process func(value Change)) {
sliceValue := change.asSlice()
for ix := 0; ix < len(sliceValue.Before) || ix < len(sliceValue.After); ix++ {
process(sliceValue.getChild(ix, ix))
value := sliceValue.getChild(ix, ix)
if !value.RelevantAttributes.MatchesPartial() {
// Mark non-relevant attributes as unchanged.
value = value.AsNoOp()
}
process(value)
}
}

View File

@@ -13,25 +13,40 @@ import (
func (change Change) computeAttributeDiffAsMap(elementType cty.Type) computed.Diff {
mapValue := change.asMap()
elements, current := collections.TransformMap(mapValue.Before, mapValue.After, func(key string) computed.Diff {
return mapValue.getChild(key).computeDiffForType(elementType)
value := mapValue.getChild(key)
if !value.RelevantAttributes.MatchesPartial() {
// Mark non-relevant attributes as unchanged.
value = value.AsNoOp()
}
return value.computeDiffForType(elementType)
})
return computed.NewDiff(renderers.Map(elements), current, change.ReplacePaths.ForcesReplacement())
return computed.NewDiff(renderers.Map(elements), current, change.ReplacePaths.Matches())
}
func (change Change) computeAttributeDiffAsNestedMap(attributes map[string]*jsonprovider.Attribute) computed.Diff {
mapValue := change.asMap()
elements, current := collections.TransformMap(mapValue.Before, mapValue.After, func(key string) computed.Diff {
return mapValue.getChild(key).computeDiffForNestedAttribute(&jsonprovider.NestedType{
value := mapValue.getChild(key)
if !value.RelevantAttributes.MatchesPartial() {
// Mark non-relevant attributes as unchanged.
value = value.AsNoOp()
}
return value.computeDiffForNestedAttribute(&jsonprovider.NestedType{
Attributes: attributes,
NestingMode: "single",
})
})
return computed.NewDiff(renderers.NestedMap(elements), current, change.ReplacePaths.ForcesReplacement())
return computed.NewDiff(renderers.NestedMap(elements), current, change.ReplacePaths.Matches())
}
func (change Change) computeBlockDiffsAsMap(block *jsonprovider.Block) (map[string]computed.Diff, plans.Action) {
mapValue := change.asMap()
return collections.TransformMap(mapValue.Before, mapValue.After, func(key string) computed.Diff {
return mapValue.getChild(key).ComputeDiffForBlock(block)
value := mapValue.getChild(key)
if !value.RelevantAttributes.MatchesPartial() {
// Mark non-relevant attributes as unchanged.
value = value.AsNoOp()
}
return value.ComputeDiffForBlock(block)
})
}

View File

@@ -14,14 +14,14 @@ func (change Change) computeAttributeDiffAsObject(attributes map[string]cty.Type
attributeDiffs, action := processObject(change, attributes, func(value Change, ctype cty.Type) computed.Diff {
return value.computeDiffForType(ctype)
})
return computed.NewDiff(renderers.Object(attributeDiffs), action, change.ReplacePaths.ForcesReplacement())
return computed.NewDiff(renderers.Object(attributeDiffs), action, change.ReplacePaths.Matches())
}
func (change Change) computeAttributeDiffAsNestedObject(attributes map[string]*jsonprovider.Attribute) computed.Diff {
attributeDiffs, action := processObject(change, attributes, func(value Change, attribute *jsonprovider.Attribute) computed.Diff {
return value.ComputeDiffForAttribute(attribute)
})
return computed.NewDiff(renderers.NestedObject(attributeDiffs), action, change.ReplacePaths.ForcesReplacement())
return computed.NewDiff(renderers.NestedObject(attributeDiffs), action, change.ReplacePaths.Matches())
}
// processObject steps through the children of value as if it is an object and
@@ -43,6 +43,11 @@ func processObject[T any](v Change, attributes map[string]T, computeDiff func(Ch
for key, attribute := range attributes {
attributeValue := mapValue.getChild(key)
if !attributeValue.RelevantAttributes.MatchesPartial() {
// Mark non-relevant attributes as unchanged.
attributeValue = attributeValue.AsNoOp()
}
// We always assume changes to object are implicit.
attributeValue.BeforeExplicit = false
attributeValue.AfterExplicit = false

View File

@@ -17,5 +17,5 @@ func (change Change) ComputeDiffForOutput() computed.Diff {
}
jsonOpts := renderers.RendererJsonOpts()
return jsonOpts.Transform(change.Before, change.After)
return jsonOpts.Transform(change.Before, change.After, change.RelevantAttributes)
}

View File

@@ -1,114 +0,0 @@
package replace
import "encoding/json"
// ForcesReplacement encapsulates the ReplacePaths logic from the Terraform
// change object.
//
// It is possible for a change to a deeply nested attribute or block to result
// in an entire resource being replaced (deleted then recreated) instead of
// simply updated. In this case, we want to attach some additional context to
// say, this resource is being replaced because of these changes to its
// internal values.
//
// The ReplacePaths field is a slice of paths that point to the values causing
// the replace operation. It's a slice of paths because you can have multiple
// internal values causing a replacement.
//
// Each path is a slice of indices, where an index can be a string or an
// integer. We represent this a slice of generic interfaces: []interface{}. This
// is because we actually parse this field from JSON and have no way to easily
// represent a value that can be a string or an integer in Go. Luckily, this
// doesn't matter too much from an implementation point of view because we
// always know what type to expect as we know whether we are currently looking
// at a list type (which means an integer) or a map type (which means a string).
//
// The GetChildWithKey and GetChildWithIndex return additional but modified
// ForcesReplacement objects, where a path is simply dropped if the index
// doesn't match or included with the first entry removed if the index did
// match. These functions are called as the outside Change objects are being
// created for a complex change's children.
//
// The ForcesReplacement function actually tells you whether the current value
// is causing a replacement operation as one of the paths will be empty since
// we removed an entry every time the path matched, and the last entry will have
// been removed when the change was created.
type ForcesReplacement struct {
ReplacePaths [][]interface{}
}
// Parse accepts a json.RawMessage and outputs a formatted ForcesReplacement
// object.
//
// Parse expects the message to be a JSON array of JSON arrays containing
// strings and floats. This function happily accepts a null input representing
// none of the changes in this resource are causing a replacement.
func Parse(message json.RawMessage) ForcesReplacement {
replace := ForcesReplacement{}
if message == nil {
return replace
}
if err := json.Unmarshal(message, &replace.ReplacePaths); err != nil {
panic("failed to unmarshal replace paths: " + err.Error())
}
return replace
}
// ForcesReplacement returns true if this ForcesReplacement object represents
// a change that is causing the entire resource to be replaced.
func (replace ForcesReplacement) ForcesReplacement() bool {
for _, path := range replace.ReplacePaths {
if len(path) == 0 {
return true
}
}
return false
}
// GetChildWithKey steps through the paths in this ForcesReplacement and checks
// if any match the specified key.
//
// This function assumes the index will all be strings, so callers have to be
// sure they have navigated through previous paths accurately to this point or
// this function is liable to panic.
func (replace ForcesReplacement) GetChildWithKey(key string) ForcesReplacement {
child := ForcesReplacement{}
for _, path := range replace.ReplacePaths {
if len(path) == 0 {
// This means that the current value is causing a replacement but
// not its children, so we skip as we are returning the child's
// value.
continue
}
if path[0].(string) == key {
child.ReplacePaths = append(child.ReplacePaths, path[1:])
}
}
return child
}
// GetChildWithIndex steps through the paths in this ForcesReplacement and
// checks if any match the specified index.
//
// This function assumes the index will all be integers, so callers have to be
// sure they have navigated through previous paths accurately to this point or
// this function is liable to panic.
func (replace ForcesReplacement) GetChildWithIndex(index int) ForcesReplacement {
child := ForcesReplacement{}
for _, path := range replace.ReplacePaths {
if len(path) == 0 {
// This means that the current value is causing a replacement but
// not its children, so we skip as we are returning the child's
// value.
continue
}
if int(path[0].(float64)) == index {
child.ReplacePaths = append(child.ReplacePaths, path[1:])
}
}
return child
}

View File

@@ -45,14 +45,15 @@ func (change Change) checkForSensitive(create CreateSensitiveRenderer, computedD
// it will just be ignored in favour of printing `(sensitive value)`.
value := Change{
BeforeExplicit: change.BeforeExplicit,
AfterExplicit: change.AfterExplicit,
Before: change.Before,
After: change.After,
Unknown: change.Unknown,
BeforeSensitive: false,
AfterSensitive: false,
ReplacePaths: change.ReplacePaths,
BeforeExplicit: change.BeforeExplicit,
AfterExplicit: change.AfterExplicit,
Before: change.Before,
After: change.After,
Unknown: change.Unknown,
BeforeSensitive: false,
AfterSensitive: false,
ReplacePaths: change.ReplacePaths,
RelevantAttributes: change.RelevantAttributes,
}
inner := computedDiff(value)
@@ -64,7 +65,7 @@ func (change Change) checkForSensitive(create CreateSensitiveRenderer, computedD
action = plans.Update
}
return computed.NewDiff(create(inner, beforeSensitive, afterSensitive), action, change.ReplacePaths.ForcesReplacement()), true
return computed.NewDiff(create(inner, beforeSensitive, afterSensitive), action, change.ReplacePaths.Matches()), true
}
func (change Change) isBeforeSensitive() bool {

View File

@@ -8,6 +8,7 @@ import (
"github.com/hashicorp/terraform/internal/command/jsonformat/collections"
"github.com/hashicorp/terraform/internal/command/jsonformat/computed"
"github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers"
"github.com/hashicorp/terraform/internal/command/jsonformat/differ/attribute_path"
"github.com/hashicorp/terraform/internal/command/jsonprovider"
"github.com/hashicorp/terraform/internal/plans"
)
@@ -20,7 +21,7 @@ func (change Change) computeAttributeDiffAsSet(elementType cty.Type) computed.Di
elements = append(elements, element)
current = collections.CompareActions(current, element.Action)
})
return computed.NewDiff(renderers.Set(elements), current, change.ReplacePaths.ForcesReplacement())
return computed.NewDiff(renderers.Set(elements), current, change.ReplacePaths.Matches())
}
func (change Change) computeAttributeDiffAsNestedSet(attributes map[string]*jsonprovider.Attribute) computed.Diff {
@@ -34,7 +35,7 @@ func (change Change) computeAttributeDiffAsNestedSet(attributes map[string]*json
elements = append(elements, element)
current = collections.CompareActions(current, element.Action)
})
return computed.NewDiff(renderers.NestedSet(elements), current, change.ReplacePaths.ForcesReplacement())
return computed.NewDiff(renderers.NestedSet(elements), current, change.ReplacePaths.Matches())
}
func (change Change) computeBlockDiffsAsSet(block *jsonprovider.Block) ([]computed.Diff, plans.Action) {
@@ -79,6 +80,29 @@ func (change Change) processSet(process func(value Change)) {
}
}
clearRelevantStatus := func(change Change) Change {
// It's actually really difficult to render the diffs when some indices
// within a slice are relevant and others aren't. To make this simpler
// we just treat all children of a relevant list or set as also
// relevant.
//
// Interestingly the terraform plan builder also agrees with this, and
// never sets relevant attributes beneath lists or sets. We're just
// going to enforce this logic here as well. If the collection is
// relevant (decided elsewhere), then every element in the collection is
// also relevant. To be clear, in practice even if we didn't do the
// following explicitly the effect would be the same. It's just nicer
// for us to be clear about the behaviour we expect.
//
// What makes this difficult is the fact that the beforeIx and afterIx
// can be different, and it's quite difficult to work out which one is
// the relevant one. For nested lists, block lists, and tuples it's much
// easier because we always process the same indices in the before and
// after.
change.RelevantAttributes = attribute_path.AlwaysMatcher()
return change
}
// Now everything in before should be a key in foundInBefore and a value
// in foundInAfter. If a key is mapped to -1 in foundInBefore it means it
// does not have an equivalent in foundInAfter and so has been deleted.
@@ -88,11 +112,11 @@ func (change Change) processSet(process func(value Change)) {
for ix := 0; ix < len(sliceValue.Before); ix++ {
if jx := foundInBefore[ix]; jx >= 0 {
child := sliceValue.getChild(ix, jx)
child := clearRelevantStatus(sliceValue.getChild(ix, jx))
process(child)
continue
}
child := sliceValue.getChild(ix, len(sliceValue.After))
child := clearRelevantStatus(sliceValue.getChild(ix, len(sliceValue.After)))
process(child)
}
@@ -101,7 +125,7 @@ func (change Change) processSet(process func(value Change)) {
// Then this value was handled in the previous for loop.
continue
}
child := sliceValue.getChild(len(sliceValue.Before), jx)
child := clearRelevantStatus(sliceValue.getChild(len(sliceValue.Before), jx))
process(child)
}
}

View File

@@ -14,9 +14,13 @@ func (change Change) computeAttributeDiffAsTuple(elementTypes []cty.Type) comput
sliceValue := change.asSlice()
for ix, elementType := range elementTypes {
childValue := sliceValue.getChild(ix, ix)
if !childValue.RelevantAttributes.MatchesPartial() {
// Mark non-relevant attributes as unchanged.
childValue = childValue.AsNoOp()
}
element := childValue.computeDiffForType(elementType)
elements = append(elements, element)
current = collections.CompareActions(current, element.Action)
}
return computed.NewDiff(renderers.List(elements), current, change.ReplacePaths.ForcesReplacement())
return computed.NewDiff(renderers.List(elements), current, change.ReplacePaths.Matches())
}

View File

@@ -64,9 +64,11 @@ func (change Change) checkForUnknown(childUnknown interface{}, computeDiff func(
// accurately.
beforeValue := Change{
Before: change.Before,
BeforeSensitive: change.BeforeSensitive,
Unknown: childUnknown,
Before: change.Before,
BeforeSensitive: change.BeforeSensitive,
Unknown: childUnknown,
ReplacePaths: change.ReplacePaths,
RelevantAttributes: change.RelevantAttributes,
}
return change.asDiff(renderers.Unknown(computeDiff(beforeValue))), true
}