opentofu/terraform/diff.go
James Bardin 0b7be2d0e3 fixes for the remaining tests
It's possible that a computed collection could be handled by the
attribute name, rather than the index count value.

Use a new testDiffFn for some tests, which don't work with the old
function that can't determine `computed` without the schema.
2018-11-19 18:56:50 -05:00

1202 lines
30 KiB
Go

package terraform
import (
"bufio"
"bytes"
"fmt"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/hcl2shim"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/zclconf/go-cty/cty"
"github.com/mitchellh/copystructure"
)
// DiffChangeType is an enum with the kind of changes a diff has planned.
type DiffChangeType byte
const (
DiffInvalid DiffChangeType = iota
DiffNone
DiffCreate
DiffUpdate
DiffDestroy
DiffDestroyCreate
// DiffRefresh is only used in the UI for displaying diffs.
// Managed resource reads never appear in plan, and when data source
// reads appear they are represented as DiffCreate in core before
// transforming to DiffRefresh in the UI layer.
DiffRefresh // TODO: Actually use DiffRefresh in core too, for less confusion
)
// multiVal matches the index key to a flatmapped set, list or map
var multiVal = regexp.MustCompile(`\.(#|%)$`)
// Diff tracks the changes that are necessary to apply a configuration
// to an existing infrastructure.
type Diff struct {
// Modules contains all the modules that have a diff
Modules []*ModuleDiff
}
// Prune cleans out unused structures in the diff without affecting
// the behavior of the diff at all.
//
// This is not safe to call concurrently. This is safe to call on a
// nil Diff.
func (d *Diff) Prune() {
if d == nil {
return
}
// Prune all empty modules
newModules := make([]*ModuleDiff, 0, len(d.Modules))
for _, m := range d.Modules {
// If the module isn't empty, we keep it
if !m.Empty() {
newModules = append(newModules, m)
}
}
if len(newModules) == 0 {
newModules = nil
}
d.Modules = newModules
}
// AddModule adds the module with the given path to the diff.
//
// This should be the preferred method to add module diffs since it
// allows us to optimize lookups later as well as control sorting.
func (d *Diff) AddModule(path addrs.ModuleInstance) *ModuleDiff {
// Lower the new-style address into a legacy-style address.
// This requires that none of the steps have instance keys, which is
// true for all addresses at the time of implementing this because
// "count" and "for_each" are not yet implemented for modules.
legacyPath := make([]string, len(path))
for i, step := range path {
if step.InstanceKey != addrs.NoKey {
// FIXME: Once the rest of Terraform is ready to use count and
// for_each, remove all of this and just write the addrs.ModuleInstance
// value itself into the ModuleState.
panic("diff cannot represent modules with count or for_each keys")
}
legacyPath[i] = step.Name
}
m := &ModuleDiff{Path: legacyPath}
m.init()
d.Modules = append(d.Modules, m)
return m
}
// ModuleByPath is used to lookup the module diff for the given path.
// This should be the preferred lookup mechanism as it allows for future
// lookup optimizations.
func (d *Diff) ModuleByPath(path addrs.ModuleInstance) *ModuleDiff {
if d == nil {
return nil
}
for _, mod := range d.Modules {
if mod.Path == nil {
panic("missing module path")
}
modPath := normalizeModulePath(mod.Path)
if modPath.String() == path.String() {
return mod
}
}
return nil
}
// RootModule returns the ModuleState for the root module
func (d *Diff) RootModule() *ModuleDiff {
root := d.ModuleByPath(addrs.RootModuleInstance)
if root == nil {
panic("missing root module")
}
return root
}
// Empty returns true if the diff has no changes.
func (d *Diff) Empty() bool {
if d == nil {
return true
}
for _, m := range d.Modules {
if !m.Empty() {
return false
}
}
return true
}
// Equal compares two diffs for exact equality.
//
// This is different from the Same comparison that is supported which
// checks for operation equality taking into account computed values. Equal
// instead checks for exact equality.
func (d *Diff) Equal(d2 *Diff) bool {
// If one is nil, they must both be nil
if d == nil || d2 == nil {
return d == d2
}
// Sort the modules
sort.Sort(moduleDiffSort(d.Modules))
sort.Sort(moduleDiffSort(d2.Modules))
// Copy since we have to modify the module destroy flag to false so
// we don't compare that. TODO: delete this when we get rid of the
// destroy flag on modules.
dCopy := d.DeepCopy()
d2Copy := d2.DeepCopy()
for _, m := range dCopy.Modules {
m.Destroy = false
}
for _, m := range d2Copy.Modules {
m.Destroy = false
}
// Use DeepEqual
return reflect.DeepEqual(dCopy, d2Copy)
}
// DeepCopy performs a deep copy of all parts of the Diff, making the
// resulting Diff safe to use without modifying this one.
func (d *Diff) DeepCopy() *Diff {
copy, err := copystructure.Config{Lock: true}.Copy(d)
if err != nil {
panic(err)
}
return copy.(*Diff)
}
func (d *Diff) String() string {
var buf bytes.Buffer
keys := make([]string, 0, len(d.Modules))
lookup := make(map[string]*ModuleDiff)
for _, m := range d.Modules {
addr := normalizeModulePath(m.Path)
key := addr.String()
keys = append(keys, key)
lookup[key] = m
}
sort.Strings(keys)
for _, key := range keys {
m := lookup[key]
mStr := m.String()
// If we're the root module, we just write the output directly.
if reflect.DeepEqual(m.Path, rootModulePath) {
buf.WriteString(mStr + "\n")
continue
}
buf.WriteString(fmt.Sprintf("%s:\n", key))
s := bufio.NewScanner(strings.NewReader(mStr))
for s.Scan() {
buf.WriteString(fmt.Sprintf(" %s\n", s.Text()))
}
}
return strings.TrimSpace(buf.String())
}
func (d *Diff) init() {
if d.Modules == nil {
rootDiff := &ModuleDiff{Path: rootModulePath}
d.Modules = []*ModuleDiff{rootDiff}
}
for _, m := range d.Modules {
m.init()
}
}
// ModuleDiff tracks the differences between resources to apply within
// a single module.
type ModuleDiff struct {
Path []string
Resources map[string]*InstanceDiff
Destroy bool // Set only by the destroy plan
}
func (d *ModuleDiff) init() {
if d.Resources == nil {
d.Resources = make(map[string]*InstanceDiff)
}
for _, r := range d.Resources {
r.init()
}
}
// ChangeType returns the type of changes that the diff for this
// module includes.
//
// At a module level, this will only be DiffNone, DiffUpdate, DiffDestroy, or
// DiffCreate. If an instance within the module has a DiffDestroyCreate
// then this will register as a DiffCreate for a module.
func (d *ModuleDiff) ChangeType() DiffChangeType {
result := DiffNone
for _, r := range d.Resources {
change := r.ChangeType()
switch change {
case DiffCreate, DiffDestroy:
if result == DiffNone {
result = change
}
case DiffDestroyCreate, DiffUpdate:
result = DiffUpdate
}
}
return result
}
// Empty returns true if the diff has no changes within this module.
func (d *ModuleDiff) Empty() bool {
if d.Destroy {
return false
}
if len(d.Resources) == 0 {
return true
}
for _, rd := range d.Resources {
if !rd.Empty() {
return false
}
}
return true
}
// Instances returns the instance diffs for the id given. This can return
// multiple instance diffs if there are counts within the resource.
func (d *ModuleDiff) Instances(id string) []*InstanceDiff {
var result []*InstanceDiff
for k, diff := range d.Resources {
if k == id || strings.HasPrefix(k, id+".") {
if !diff.Empty() {
result = append(result, diff)
}
}
}
return result
}
// IsRoot says whether or not this module diff is for the root module.
func (d *ModuleDiff) IsRoot() bool {
return reflect.DeepEqual(d.Path, rootModulePath)
}
// String outputs the diff in a long but command-line friendly output
// format that users can read to quickly inspect a diff.
func (d *ModuleDiff) String() string {
var buf bytes.Buffer
names := make([]string, 0, len(d.Resources))
for name, _ := range d.Resources {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
rdiff := d.Resources[name]
crud := "UPDATE"
switch {
case rdiff.RequiresNew() && (rdiff.GetDestroy() || rdiff.GetDestroyTainted()):
crud = "DESTROY/CREATE"
case rdiff.GetDestroy() || rdiff.GetDestroyDeposed():
crud = "DESTROY"
case rdiff.RequiresNew():
crud = "CREATE"
}
extra := ""
if !rdiff.GetDestroy() && rdiff.GetDestroyDeposed() {
extra = " (deposed only)"
}
buf.WriteString(fmt.Sprintf(
"%s: %s%s\n",
crud,
name,
extra))
keyLen := 0
rdiffAttrs := rdiff.CopyAttributes()
keys := make([]string, 0, len(rdiffAttrs))
for key, _ := range rdiffAttrs {
if key == "id" {
continue
}
keys = append(keys, key)
if len(key) > keyLen {
keyLen = len(key)
}
}
sort.Strings(keys)
for _, attrK := range keys {
attrDiff, _ := rdiff.GetAttribute(attrK)
v := attrDiff.New
u := attrDiff.Old
if attrDiff.NewComputed {
v = "<computed>"
}
if attrDiff.Sensitive {
u = "<sensitive>"
v = "<sensitive>"
}
updateMsg := ""
if attrDiff.RequiresNew {
updateMsg = " (forces new resource)"
} else if attrDiff.Sensitive {
updateMsg = " (attribute changed)"
}
buf.WriteString(fmt.Sprintf(
" %s:%s %#v => %#v%s\n",
attrK,
strings.Repeat(" ", keyLen-len(attrK)),
u,
v,
updateMsg))
}
}
return buf.String()
}
// InstanceDiff is the diff of a resource from some state to another.
type InstanceDiff struct {
mu sync.Mutex
Attributes map[string]*ResourceAttrDiff
Destroy bool
DestroyDeposed bool
DestroyTainted bool
// Meta is a simple K/V map that is stored in a diff and persisted to
// plans but otherwise is completely ignored by Terraform core. It is
// meant to be used for additional data a resource may want to pass through.
// The value here must only contain Go primitives and collections.
Meta map[string]interface{}
}
func (d *InstanceDiff) Lock() { d.mu.Lock() }
func (d *InstanceDiff) Unlock() { d.mu.Unlock() }
// ApplyToValue merges the receiver into the given base value, returning a
// new value that incorporates the planned changes. The given value must
// conform to the given schema, or this method will panic.
//
// This method is intended for shimming old subsystems that still use this
// legacy diff type to work with the new-style types.
func (d *InstanceDiff) ApplyToValue(base cty.Value, schema *configschema.Block) (cty.Value, error) {
// Create an InstanceState attributes from our existing state.
// We can use this to more easily apply the diff changes.
attrs := hcl2shim.FlatmapValueFromHCL2(base)
applied, err := d.Apply(attrs, schema)
if err != nil {
return base, err
}
val, err := hcl2shim.HCL2ValueFromFlatmap(applied, schema.ImpliedType())
if err != nil {
return base, err
}
return schema.CoerceValue(val)
}
// Apply applies the diff to the provided flatmapped attributes,
// returning the new instance attributes.
//
// This method is intended for shimming old subsystems that still use this
// legacy diff type to work with the new-style types.
func (d *InstanceDiff) Apply(attrs map[string]string, schema *configschema.Block) (map[string]string, error) {
// We always build a new value here, even if the given diff is "empty",
// because we might be planning to create a new instance that happens
// to have no attributes set, and so we want to produce an empty object
// rather than just echoing back the null old value.
if attrs == nil {
attrs = map[string]string{}
}
return d.applyDiff(attrs, schema)
}
func (d *InstanceDiff) applyDiff(attrs map[string]string, schema *configschema.Block) (map[string]string, error) {
// Rather applying the diff to mutate the attrs, we'll copy new values into
// here to avoid the possibility of leaving stale values.
result := map[string]string{}
if d.Destroy || d.DestroyDeposed || d.DestroyTainted {
return result, nil
}
// iterate over the schema rather than the attributes, so we can handle
// blocks separately from plain attributes
for name, attrSchema := range schema.Attributes {
var err error
var newAttrs map[string]string
// handle non-block collections
switch ty := attrSchema.Type; {
case ty.IsListType() || ty.IsTupleType() || ty.IsMapType():
newAttrs, err = d.applyCollectionDiff(name, attrs, attrSchema)
case ty.IsSetType():
newAttrs, err = d.applySetDiff(name, attrs, schema)
default:
newAttrs, err = d.applyAttrDiff(name, attrs, attrSchema)
}
if err != nil {
return result, err
}
for k, v := range newAttrs {
result[k] = v
}
}
for name, block := range schema.BlockTypes {
newAttrs, err := d.applySetDiff(name, attrs, &block.Block)
if err != nil {
return result, err
}
for k, v := range newAttrs {
result[k] = v
}
}
return result, nil
}
func (d *InstanceDiff) applyAttrDiff(attrName string, oldAttrs map[string]string, attrSchema *configschema.Attribute) (map[string]string, error) {
result := map[string]string{}
diff := d.Attributes[attrName]
old, exists := oldAttrs[attrName]
if diff != nil && diff.NewComputed {
result[attrName] = config.UnknownVariableValue
return result, nil
}
if attrName == "id" {
if old == "" {
result["id"] = config.UnknownVariableValue
} else {
result["id"] = old
}
return result, nil
}
// attribute diffs are sometimes missed, so assume no diff means keep the
// old value
if diff == nil {
if exists {
result[attrName] = old
} else {
// We need required values, so set those with an empty value. It
// must be set in the config, since if it were missing it would have
// failed validation.
if attrSchema.Required {
result[attrName] = ""
}
}
return result, nil
}
// check for missmatched diff values
if exists &&
old != diff.Old &&
old != config.UnknownVariableValue &&
diff.Old != config.UnknownVariableValue {
return result, fmt.Errorf("diff apply conflict for %s: diff expects %q, but prior value has %q", attrName, diff.Old, old)
}
if attrSchema.Computed && diff.NewComputed {
result[attrName] = config.UnknownVariableValue
return result, nil
}
if diff.NewRemoved {
// don't set anything in the new value
return result, nil
}
result[attrName] = diff.New
return result, nil
}
func (d *InstanceDiff) applyCollectionDiff(attrName string, oldAttrs map[string]string, attrSchema *configschema.Attribute) (map[string]string, error) {
result := map[string]string{}
// check the index first for special handling
for k, diff := range d.Attributes {
// check the index value, which can be set, and 0
if k == attrName+".#" || k == attrName+".%" || k == attrName {
if diff.NewRemoved {
return result, nil
}
if diff.NewComputed {
result[k] = config.UnknownVariableValue
return result, nil
}
// do what the diff tells us to here, so that it's consistent with applies
if diff.New == "0" {
result[k] = "0"
return result, nil
}
}
}
// collect all the keys from the diff and the old state
keys := map[string]bool{}
for k := range d.Attributes {
keys[k] = true
}
for k := range oldAttrs {
keys[k] = true
}
idx := attrName + ".#"
if attrSchema.Type.IsMapType() {
idx = attrName + ".%"
}
for k := range keys {
if !strings.HasPrefix(k, attrName+".") {
continue
}
res, err := d.applyAttrDiff(k, oldAttrs, attrSchema)
if err != nil {
return result, err
}
for k, v := range res {
result[k] = v
}
}
// Don't trust helper/schema to return a valid count, or even have one at
// all.
result[idx] = countFlatmapContainerValues(idx, result)
return result, nil
}
func (d *InstanceDiff) applySetDiff(attrName string, oldAttrs map[string]string, block *configschema.Block) (map[string]string, error) {
result := map[string]string{}
idx := attrName + ".#"
// first find the index diff
for k, diff := range d.Attributes {
if k != idx {
continue
}
if diff.NewComputed {
result[k] = config.UnknownVariableValue
return result, nil
}
}
// Flag if there was a diff used in the set at all.
// If not, take the pre-existing set values
setDiff := false
// here we're trusting the diff to supply all the known items
for k, diff := range d.Attributes {
if !strings.HasPrefix(k, attrName+".") {
continue
}
setDiff = true
if diff.NewRemoved {
// no longer in the set
continue
}
if diff.NewComputed {
result[k] = config.UnknownVariableValue
continue
}
// helper/schema doesn't list old removed values, but since the set
// exists NewRemoved may not be true.
if diff.New == "" && diff.Old == "" {
continue
}
result[k] = diff.New
}
// use the existing values if there was no set diff at all
if !setDiff {
for k, v := range oldAttrs {
if strings.HasPrefix(k, attrName+".") {
result[k] = v
}
}
}
result[idx] = countFlatmapContainerValues(idx, result)
return result, nil
}
// countFlatmapContainerValues returns the number of values in the flatmapped container
// (set, map, list) indexed by key. The key argument is expected to include the
// trailing ".#", or ".%".
func countFlatmapContainerValues(key string, attrs map[string]string) string {
if len(key) < 3 || !(strings.HasSuffix(key, ".#") || strings.HasSuffix(key, ".%")) {
panic(fmt.Sprintf("invalid index value %q", key))
}
prefix := key[:len(key)-1]
items := map[string]int{}
for k := range attrs {
if k == key {
continue
}
if !strings.HasPrefix(k, prefix) {
continue
}
suffix := k[len(prefix):]
dot := strings.Index(suffix, ".")
if dot > 0 {
suffix = suffix[:dot]
}
items[suffix]++
}
return strconv.Itoa(len(items))
}
// ResourceAttrDiff is the diff of a single attribute of a resource.
type ResourceAttrDiff struct {
Old string // Old Value
New string // New Value
NewComputed bool // True if new value is computed (unknown currently)
NewRemoved bool // True if this attribute is being removed
NewExtra interface{} // Extra information for the provider
RequiresNew bool // True if change requires new resource
Sensitive bool // True if the data should not be displayed in UI output
Type DiffAttrType
}
// Empty returns true if the diff for this attr is neutral
func (d *ResourceAttrDiff) Empty() bool {
return d.Old == d.New && !d.NewComputed && !d.NewRemoved
}
func (d *ResourceAttrDiff) GoString() string {
return fmt.Sprintf("*%#v", *d)
}
// DiffAttrType is an enum type that says whether a resource attribute
// diff is an input attribute (comes from the configuration) or an
// output attribute (comes as a result of applying the configuration). An
// example input would be "ami" for AWS and an example output would be
// "private_ip".
type DiffAttrType byte
const (
DiffAttrUnknown DiffAttrType = iota
DiffAttrInput
DiffAttrOutput
)
func (d *InstanceDiff) init() {
if d.Attributes == nil {
d.Attributes = make(map[string]*ResourceAttrDiff)
}
}
func NewInstanceDiff() *InstanceDiff {
return &InstanceDiff{Attributes: make(map[string]*ResourceAttrDiff)}
}
func (d *InstanceDiff) Copy() (*InstanceDiff, error) {
if d == nil {
return nil, nil
}
dCopy, err := copystructure.Config{Lock: true}.Copy(d)
if err != nil {
return nil, err
}
return dCopy.(*InstanceDiff), nil
}
// ChangeType returns the DiffChangeType represented by the diff
// for this single instance.
func (d *InstanceDiff) ChangeType() DiffChangeType {
if d.Empty() {
return DiffNone
}
if d.RequiresNew() && (d.GetDestroy() || d.GetDestroyTainted()) {
return DiffDestroyCreate
}
if d.GetDestroy() || d.GetDestroyDeposed() {
return DiffDestroy
}
if d.RequiresNew() {
return DiffCreate
}
return DiffUpdate
}
// Empty returns true if this diff encapsulates no changes.
func (d *InstanceDiff) Empty() bool {
if d == nil {
return true
}
d.mu.Lock()
defer d.mu.Unlock()
return !d.Destroy &&
!d.DestroyTainted &&
!d.DestroyDeposed &&
len(d.Attributes) == 0
}
// Equal compares two diffs for exact equality.
//
// This is different from the Same comparison that is supported which
// checks for operation equality taking into account computed values. Equal
// instead checks for exact equality.
func (d *InstanceDiff) Equal(d2 *InstanceDiff) bool {
// If one is nil, they must both be nil
if d == nil || d2 == nil {
return d == d2
}
// Use DeepEqual
return reflect.DeepEqual(d, d2)
}
// DeepCopy performs a deep copy of all parts of the InstanceDiff
func (d *InstanceDiff) DeepCopy() *InstanceDiff {
copy, err := copystructure.Config{Lock: true}.Copy(d)
if err != nil {
panic(err)
}
return copy.(*InstanceDiff)
}
func (d *InstanceDiff) GoString() string {
return fmt.Sprintf("*%#v", InstanceDiff{
Attributes: d.Attributes,
Destroy: d.Destroy,
DestroyTainted: d.DestroyTainted,
DestroyDeposed: d.DestroyDeposed,
})
}
// RequiresNew returns true if the diff requires the creation of a new
// resource (implying the destruction of the old).
func (d *InstanceDiff) RequiresNew() bool {
if d == nil {
return false
}
d.mu.Lock()
defer d.mu.Unlock()
return d.requiresNew()
}
func (d *InstanceDiff) requiresNew() bool {
if d == nil {
return false
}
if d.DestroyTainted {
return true
}
for _, rd := range d.Attributes {
if rd != nil && rd.RequiresNew {
return true
}
}
return false
}
func (d *InstanceDiff) GetDestroyDeposed() bool {
d.mu.Lock()
defer d.mu.Unlock()
return d.DestroyDeposed
}
func (d *InstanceDiff) SetDestroyDeposed(b bool) {
d.mu.Lock()
defer d.mu.Unlock()
d.DestroyDeposed = b
}
// These methods are properly locked, for use outside other InstanceDiff
// methods but everywhere else within the terraform package.
// TODO refactor the locking scheme
func (d *InstanceDiff) SetTainted(b bool) {
d.mu.Lock()
defer d.mu.Unlock()
d.DestroyTainted = b
}
func (d *InstanceDiff) GetDestroyTainted() bool {
d.mu.Lock()
defer d.mu.Unlock()
return d.DestroyTainted
}
func (d *InstanceDiff) SetDestroy(b bool) {
d.mu.Lock()
defer d.mu.Unlock()
d.Destroy = b
}
func (d *InstanceDiff) GetDestroy() bool {
d.mu.Lock()
defer d.mu.Unlock()
return d.Destroy
}
func (d *InstanceDiff) SetAttribute(key string, attr *ResourceAttrDiff) {
d.mu.Lock()
defer d.mu.Unlock()
d.Attributes[key] = attr
}
func (d *InstanceDiff) DelAttribute(key string) {
d.mu.Lock()
defer d.mu.Unlock()
delete(d.Attributes, key)
}
func (d *InstanceDiff) GetAttribute(key string) (*ResourceAttrDiff, bool) {
d.mu.Lock()
defer d.mu.Unlock()
attr, ok := d.Attributes[key]
return attr, ok
}
func (d *InstanceDiff) GetAttributesLen() int {
d.mu.Lock()
defer d.mu.Unlock()
return len(d.Attributes)
}
// Safely copies the Attributes map
func (d *InstanceDiff) CopyAttributes() map[string]*ResourceAttrDiff {
d.mu.Lock()
defer d.mu.Unlock()
attrs := make(map[string]*ResourceAttrDiff)
for k, v := range d.Attributes {
attrs[k] = v
}
return attrs
}
// Same checks whether or not two InstanceDiff's are the "same". When
// we say "same", it is not necessarily exactly equal. Instead, it is
// just checking that the same attributes are changing, a destroy
// isn't suddenly happening, etc.
func (d *InstanceDiff) Same(d2 *InstanceDiff) (bool, string) {
// we can safely compare the pointers without a lock
switch {
case d == nil && d2 == nil:
return true, ""
case d == nil || d2 == nil:
return false, "one nil"
case d == d2:
return true, ""
}
d.mu.Lock()
defer d.mu.Unlock()
// If we're going from requiring new to NOT requiring new, then we have
// to see if all required news were computed. If so, it is allowed since
// computed may also mean "same value and therefore not new".
oldNew := d.requiresNew()
newNew := d2.RequiresNew()
if oldNew && !newNew {
oldNew = false
// This section builds a list of ignorable attributes for requiresNew
// by removing off any elements of collections going to zero elements.
// For collections going to zero, they may not exist at all in the
// new diff (and hence RequiresNew == false).
ignoreAttrs := make(map[string]struct{})
for k, diffOld := range d.Attributes {
if !strings.HasSuffix(k, ".%") && !strings.HasSuffix(k, ".#") {
continue
}
// This case is in here as a protection measure. The bug that this
// code originally fixed (GH-11349) didn't have to deal with computed
// so I'm not 100% sure what the correct behavior is. Best to leave
// the old behavior.
if diffOld.NewComputed {
continue
}
// We're looking for the case a map goes to exactly 0.
if diffOld.New != "0" {
continue
}
// Found it! Ignore all of these. The prefix here is stripping
// off the "%" so it is just "k."
prefix := k[:len(k)-1]
for k2, _ := range d.Attributes {
if strings.HasPrefix(k2, prefix) {
ignoreAttrs[k2] = struct{}{}
}
}
}
for k, rd := range d.Attributes {
if _, ok := ignoreAttrs[k]; ok {
continue
}
// If the field is requires new and NOT computed, then what
// we have is a diff mismatch for sure. We set that the old
// diff does REQUIRE a ForceNew.
if rd != nil && rd.RequiresNew && !rd.NewComputed {
oldNew = true
break
}
}
}
if oldNew != newNew {
return false, fmt.Sprintf(
"diff RequiresNew; old: %t, new: %t", oldNew, newNew)
}
// Verify that destroy matches. The second boolean here allows us to
// have mismatching Destroy if we're moving from RequiresNew true
// to false above. Therefore, the second boolean will only pass if
// we're moving from Destroy: true to false as well.
if d.Destroy != d2.GetDestroy() && d.requiresNew() == oldNew {
return false, fmt.Sprintf(
"diff: Destroy; old: %t, new: %t", d.Destroy, d2.GetDestroy())
}
// Go through the old diff and make sure the new diff has all the
// same attributes. To start, build up the check map to be all the keys.
checkOld := make(map[string]struct{})
checkNew := make(map[string]struct{})
for k, _ := range d.Attributes {
checkOld[k] = struct{}{}
}
for k, _ := range d2.CopyAttributes() {
checkNew[k] = struct{}{}
}
// Make an ordered list so we are sure the approximated hashes are left
// to process at the end of the loop
keys := make([]string, 0, len(d.Attributes))
for k, _ := range d.Attributes {
keys = append(keys, k)
}
sort.StringSlice(keys).Sort()
for _, k := range keys {
diffOld := d.Attributes[k]
if _, ok := checkOld[k]; !ok {
// We're not checking this key for whatever reason (see where
// check is modified).
continue
}
// Remove this key since we'll never hit it again
delete(checkOld, k)
delete(checkNew, k)
_, ok := d2.GetAttribute(k)
if !ok {
// If there's no new attribute, and the old diff expected the attribute
// to be removed, that's just fine.
if diffOld.NewRemoved {
continue
}
// If the last diff was a computed value then the absense of
// that value is allowed since it may mean the value ended up
// being the same.
if diffOld.NewComputed {
ok = true
}
// No exact match, but maybe this is a set containing computed
// values. So check if there is an approximate hash in the key
// and if so, try to match the key.
if strings.Contains(k, "~") {
parts := strings.Split(k, ".")
parts2 := append([]string(nil), parts...)
re := regexp.MustCompile(`^~\d+$`)
for i, part := range parts {
if re.MatchString(part) {
// we're going to consider this the base of a
// computed hash, and remove all longer matching fields
ok = true
parts2[i] = `\d+`
parts2 = parts2[:i+1]
break
}
}
re, err := regexp.Compile("^" + strings.Join(parts2, `\.`))
if err != nil {
return false, fmt.Sprintf("regexp failed to compile; err: %#v", err)
}
for k2, _ := range checkNew {
if re.MatchString(k2) {
delete(checkNew, k2)
}
}
}
// This is a little tricky, but when a diff contains a computed
// list, set, or map that can only be interpolated after the apply
// command has created the dependent resources, it could turn out
// that the result is actually the same as the existing state which
// would remove the key from the diff.
if diffOld.NewComputed && (strings.HasSuffix(k, ".#") || strings.HasSuffix(k, ".%")) {
ok = true
}
// Similarly, in a RequiresNew scenario, a list that shows up in the plan
// diff can disappear from the apply diff, which is calculated from an
// empty state.
if d.requiresNew() && (strings.HasSuffix(k, ".#") || strings.HasSuffix(k, ".%")) {
ok = true
}
if !ok {
return false, fmt.Sprintf("attribute mismatch: %s", k)
}
}
// search for the suffix of the base of a [computed] map, list or set.
match := multiVal.FindStringSubmatch(k)
if diffOld.NewComputed && len(match) == 2 {
matchLen := len(match[1])
// This is a computed list, set, or map, so remove any keys with
// this prefix from the check list.
kprefix := k[:len(k)-matchLen]
for k2, _ := range checkOld {
if strings.HasPrefix(k2, kprefix) {
delete(checkOld, k2)
}
}
for k2, _ := range checkNew {
if strings.HasPrefix(k2, kprefix) {
delete(checkNew, k2)
}
}
}
// We don't compare the values because we can't currently actually
// guarantee to generate the same value two two diffs created from
// the same state+config: we have some pesky interpolation functions
// that do not behave as pure functions (uuid, timestamp) and so they
// can be different each time a diff is produced.
// FIXME: Re-organize our config handling so that we don't re-evaluate
// expressions when we produce a second comparison diff during
// apply (for EvalCompareDiff).
}
// Check for leftover attributes
if len(checkNew) > 0 {
extras := make([]string, 0, len(checkNew))
for attr, _ := range checkNew {
extras = append(extras, attr)
}
return false,
fmt.Sprintf("extra attributes: %s", strings.Join(extras, ", "))
}
return true, ""
}
// moduleDiffSort implements sort.Interface to sort module diffs by path.
type moduleDiffSort []*ModuleDiff
func (s moduleDiffSort) Len() int { return len(s) }
func (s moduleDiffSort) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s moduleDiffSort) Less(i, j int) bool {
a := s[i]
b := s[j]
// If the lengths are different, then the shorter one always wins
if len(a.Path) != len(b.Path) {
return len(a.Path) < len(b.Path)
}
// Otherwise, compare lexically
return strings.Join(a.Path, ".") < strings.Join(b.Path, ".")
}