mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-03-27 11:00:18 -04:00
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:
201
internal/command/jsonformat/differ/attribute_path/matcher.go
Normal file
201
internal/command/jsonformat/differ/attribute_path/matcher.go
Normal 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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user