From 2d71f54fc0b3a4311c0348ca24da7c675080e28a Mon Sep 17 00:00:00 2001 From: James Bardin Date: Wed, 18 Nov 2020 14:42:21 -0500 Subject: [PATCH] remove legacy diff types --- terraform/diff.go | 1432 ---------------------------------------- terraform/diff_test.go | 1252 ----------------------------------- 2 files changed, 2684 deletions(-) delete mode 100644 terraform/diff.go delete mode 100644 terraform/diff_test.go diff --git a/terraform/diff.go b/terraform/diff.go deleted file mode 100644 index cee39cadb..000000000 --- a/terraform/diff.go +++ /dev/null @@ -1,1432 +0,0 @@ -package terraform - -import ( - "bufio" - "bytes" - "fmt" - "log" - "reflect" - "regexp" - "sort" - "strconv" - "strings" - "sync" - - "github.com/hashicorp/terraform/addrs" - "github.com/hashicorp/terraform/configs/configschema" - "github.com/hashicorp/terraform/configs/hcl2shim" - "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 { - 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 := addrs.ModuleInstance{} //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() { -} - -// 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 { - panic("not implemented") -} - -// 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 = "" - } - - if attrDiff.Sensitive { - u = "" - v = "" - } - - 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{} - } - - // 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 - } - - return d.applyBlockDiff(nil, attrs, schema) -} - -func (d *InstanceDiff) applyBlockDiff(path []string, attrs map[string]string, schema *configschema.Block) (map[string]string, error) { - result := map[string]string{} - name := "" - if len(path) > 0 { - name = path[len(path)-1] - } - - // localPrefix is used to build the local result map - localPrefix := "" - if name != "" { - localPrefix = name + "." - } - - // iterate over the schema rather than the attributes, so we can handle - // different block types separately from plain attributes - for n, attrSchema := range schema.Attributes { - var err error - newAttrs, err := d.applyAttrDiff(append(path, n), attrs, attrSchema) - - if err != nil { - return result, err - } - - for k, v := range newAttrs { - result[localPrefix+k] = v - } - } - - blockPrefix := strings.Join(path, ".") - if blockPrefix != "" { - blockPrefix += "." - } - for n, block := range schema.BlockTypes { - // we need to find the set of all keys that traverse this block - candidateKeys := map[string]bool{} - blockKey := blockPrefix + n + "." - localBlockPrefix := localPrefix + n + "." - - // we can only trust the diff for sets, since the path changes, so don't - // count existing values as candidate keys. If it turns out we're - // keeping the attributes, we will catch it down below with "keepBlock" - // after we check the set count. - if block.Nesting != configschema.NestingSet { - for k := range attrs { - if strings.HasPrefix(k, blockKey) { - nextDot := strings.Index(k[len(blockKey):], ".") - if nextDot < 0 { - continue - } - nextDot += len(blockKey) - candidateKeys[k[len(blockKey):nextDot]] = true - } - } - } - - for k, diff := range d.Attributes { - // helper/schema should not insert nil diff values, but don't panic - // if it does. - if diff == nil { - continue - } - - if strings.HasPrefix(k, blockKey) { - nextDot := strings.Index(k[len(blockKey):], ".") - if nextDot < 0 { - continue - } - - if diff.NewRemoved { - continue - } - - nextDot += len(blockKey) - candidateKeys[k[len(blockKey):nextDot]] = true - } - } - - // check each set candidate to see if it was removed. - // we need to do this, because when entire sets are removed, they may - // have the wrong key, and ony show diffs going to "" - if block.Nesting == configschema.NestingSet { - for k := range candidateKeys { - indexPrefix := strings.Join(append(path, n, k), ".") + "." - keep := false - // now check each set element to see if it's a new diff, or one - // that we're dropping. Since we're only applying the "New" - // portion of the set, we can ignore diffs that only contain "Old" - for attr, diff := range d.Attributes { - // helper/schema should not insert nil diff values, but don't panic - // if it does. - if diff == nil { - continue - } - - if !strings.HasPrefix(attr, indexPrefix) { - continue - } - - // check for empty "count" keys - if (strings.HasSuffix(attr, ".#") || strings.HasSuffix(attr, ".%")) && diff.New == "0" { - continue - } - - // removed items don't count either - if diff.NewRemoved { - continue - } - - // this must be a diff to keep - keep = true - break - } - if !keep { - delete(candidateKeys, k) - } - } - } - - for k := range candidateKeys { - newAttrs, err := d.applyBlockDiff(append(path, n, k), attrs, &block.Block) - if err != nil { - return result, err - } - - for attr, v := range newAttrs { - result[localBlockPrefix+attr] = v - } - } - - keepBlock := true - // check this block's count diff directly first, since we may not - // have candidates because it was removed and only set to "0" - if diff, ok := d.Attributes[blockKey+"#"]; ok { - if diff.New == "0" || diff.NewRemoved { - keepBlock = false - } - } - - // if there was no diff at all, then we need to keep the block attributes - if len(candidateKeys) == 0 && keepBlock { - for k, v := range attrs { - if strings.HasPrefix(k, blockKey) { - // we need the key relative to this block, so remove the - // entire prefix, then re-insert the block name. - localKey := localBlockPrefix + k[len(blockKey):] - result[localKey] = v - } - } - } - - countAddr := strings.Join(append(path, n, "#"), ".") - if countDiff, ok := d.Attributes[countAddr]; ok { - if countDiff.NewComputed { - result[localBlockPrefix+"#"] = hcl2shim.UnknownVariableValue - } else { - result[localBlockPrefix+"#"] = countDiff.New - - // While sets are complete, list are not, and we may not have all the - // information to track removals. If the list was truncated, we need to - // remove the extra items from the result. - if block.Nesting == configschema.NestingList && - countDiff.New != "" && countDiff.New != hcl2shim.UnknownVariableValue { - length, _ := strconv.Atoi(countDiff.New) - for k := range result { - if !strings.HasPrefix(k, localBlockPrefix) { - continue - } - - index := k[len(localBlockPrefix):] - nextDot := strings.Index(index, ".") - if nextDot < 1 { - continue - } - index = index[:nextDot] - i, err := strconv.Atoi(index) - if err != nil { - // this shouldn't happen since we added these - // ourself, but make note of it just in case. - log.Printf("[ERROR] bad list index in %q: %s", k, err) - continue - } - if i >= length { - delete(result, k) - } - } - } - } - } else if origCount, ok := attrs[countAddr]; ok && keepBlock { - result[localBlockPrefix+"#"] = origCount - } else { - result[localBlockPrefix+"#"] = countFlatmapContainerValues(localBlockPrefix+"#", result) - } - } - - return result, nil -} - -func (d *InstanceDiff) applyAttrDiff(path []string, attrs map[string]string, attrSchema *configschema.Attribute) (map[string]string, error) { - ty := attrSchema.Type - switch { - case ty.IsListType(), ty.IsTupleType(), ty.IsMapType(): - return d.applyCollectionDiff(path, attrs, attrSchema) - case ty.IsSetType(): - return d.applySetDiff(path, attrs, attrSchema) - default: - return d.applySingleAttrDiff(path, attrs, attrSchema) - } -} - -func (d *InstanceDiff) applySingleAttrDiff(path []string, attrs map[string]string, attrSchema *configschema.Attribute) (map[string]string, error) { - currentKey := strings.Join(path, ".") - - attr := path[len(path)-1] - - result := map[string]string{} - diff := d.Attributes[currentKey] - old, exists := attrs[currentKey] - - if diff != nil && diff.NewComputed { - result[attr] = hcl2shim.UnknownVariableValue - return result, nil - } - - // "id" must exist and not be an empty string, or it must be unknown. - // This only applied to top-level "id" fields. - if attr == "id" && len(path) == 1 { - if old == "" { - result[attr] = hcl2shim.UnknownVariableValue - } else { - result[attr] = old - } - return result, nil - } - - // attribute diffs are sometimes missed, so assume no diff means keep the - // old value - if diff == nil { - if exists { - result[attr] = 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 { - // we only set a missing string here, since bool or number types - // would have distinct zero value which shouldn't have been - // lost. - if attrSchema.Type == cty.String { - result[attr] = "" - } - } - } - return result, nil - } - - // check for missmatched diff values - if exists && - old != diff.Old && - old != hcl2shim.UnknownVariableValue && - diff.Old != hcl2shim.UnknownVariableValue { - return result, fmt.Errorf("diff apply conflict for %s: diff expects %q, but prior value has %q", attr, diff.Old, old) - } - - if diff.NewRemoved { - // don't set anything in the new value - return map[string]string{}, nil - } - - if diff.Old == diff.New && diff.New == "" { - // this can only be a valid empty string - if attrSchema.Type == cty.String { - result[attr] = "" - } - return result, nil - } - - if attrSchema.Computed && diff.NewComputed { - result[attr] = hcl2shim.UnknownVariableValue - return result, nil - } - - result[attr] = diff.New - - return result, nil -} - -func (d *InstanceDiff) applyCollectionDiff(path []string, attrs map[string]string, attrSchema *configschema.Attribute) (map[string]string, error) { - result := map[string]string{} - - prefix := "" - if len(path) > 1 { - prefix = strings.Join(path[:len(path)-1], ".") + "." - } - - name := "" - if len(path) > 0 { - name = path[len(path)-1] - } - - currentKey := prefix + name - - // 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 == currentKey+".#" || k == currentKey+".%" || k == currentKey { - if diff.NewRemoved { - return result, nil - } - - if diff.NewComputed { - result[k[len(prefix):]] = hcl2shim.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[len(prefix):]] = "0" - return result, nil - } - } - } - - // collect all the keys from the diff and the old state - noDiff := true - keys := map[string]bool{} - for k := range d.Attributes { - if !strings.HasPrefix(k, currentKey+".") { - continue - } - noDiff = false - keys[k] = true - } - - noAttrs := true - for k := range attrs { - if !strings.HasPrefix(k, currentKey+".") { - continue - } - noAttrs = false - keys[k] = true - } - - // If there's no diff and no attrs, then there's no value at all. - // This prevents an unexpected zero-count attribute in the attributes. - if noDiff && noAttrs { - return result, nil - } - - idx := "#" - if attrSchema.Type.IsMapType() { - idx = "%" - } - - for k := range keys { - // generate an schema placeholder for the values - elSchema := &configschema.Attribute{ - Type: attrSchema.Type.ElementType(), - } - - res, err := d.applySingleAttrDiff(append(path, k[len(currentKey)+1:]), attrs, elSchema) - if err != nil { - return result, err - } - - for k, v := range res { - result[name+"."+k] = v - } - } - - // Just like in nested list blocks, for simple lists we may need to fill in - // missing empty strings. - countKey := name + "." + idx - count := result[countKey] - length, _ := strconv.Atoi(count) - - if count != "" && count != hcl2shim.UnknownVariableValue && - attrSchema.Type.Equals(cty.List(cty.String)) { - // insert empty strings into missing indexes - for i := 0; i < length; i++ { - key := fmt.Sprintf("%s.%d", name, i) - if _, ok := result[key]; !ok { - result[key] = "" - } - } - } - - // now check for truncation in any type of list - if attrSchema.Type.IsListType() { - for key := range result { - if key == countKey { - continue - } - - if len(key) <= len(name)+1 { - // not sure what this is, but don't panic - continue - } - - index := key[len(name)+1:] - - // It is possible to have nested sets or maps, so look for another dot - dot := strings.Index(index, ".") - if dot > 0 { - index = index[:dot] - } - - // This shouldn't have any more dots, since the element type is only string. - num, err := strconv.Atoi(index) - if err != nil { - log.Printf("[ERROR] bad list index in %q: %s", currentKey, err) - continue - } - - if num >= length { - delete(result, key) - } - } - } - - // Fill in the count value if it wasn't present in the diff for some reason, - // or if there is no count at all. - _, countDiff := d.Attributes[countKey] - if result[countKey] == "" || (!countDiff && len(keys) != len(result)) { - result[countKey] = countFlatmapContainerValues(countKey, result) - } - - return result, nil -} - -func (d *InstanceDiff) applySetDiff(path []string, attrs map[string]string, attrSchema *configschema.Attribute) (map[string]string, error) { - // We only need this special behavior for sets of object. - if !attrSchema.Type.ElementType().IsObjectType() { - // The normal collection apply behavior will work okay for this one, then. - return d.applyCollectionDiff(path, attrs, attrSchema) - } - - // When we're dealing with a set of an object type we actually want to - // use our normal _block type_ apply behaviors, so we'll construct ourselves - // a synthetic schema that treats the object type as a block type and - // then delegate to our block apply method. - synthSchema := &configschema.Block{ - Attributes: make(map[string]*configschema.Attribute), - } - - for name, ty := range attrSchema.Type.ElementType().AttributeTypes() { - // We can safely make everything into an attribute here because in the - // event that there are nested set attributes we'll end up back in - // here again recursively and can then deal with the next level of - // expansion. - synthSchema.Attributes[name] = &configschema.Attribute{ - Type: ty, - Optional: true, - } - } - - parentPath := path[:len(path)-1] - childName := path[len(path)-1] - containerSchema := &configschema.Block{ - BlockTypes: map[string]*configschema.NestedBlock{ - childName: { - Nesting: configschema.NestingSet, - Block: *synthSchema, - }, - }, - } - - return d.applyBlockDiff(parentPath, attrs, containerSchema) -} - -// 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, ".") -} diff --git a/terraform/diff_test.go b/terraform/diff_test.go deleted file mode 100644 index e7ee0d818..000000000 --- a/terraform/diff_test.go +++ /dev/null @@ -1,1252 +0,0 @@ -package terraform - -import ( - "fmt" - "reflect" - "strconv" - "strings" - "testing" - - "github.com/hashicorp/terraform/addrs" -) - -func TestDiffEmpty(t *testing.T) { - var diff *Diff - if !diff.Empty() { - t.Fatal("should be empty") - } - - diff = new(Diff) - if !diff.Empty() { - t.Fatal("should be empty") - } - - mod := diff.AddModule(addrs.RootModuleInstance) - mod.Resources["nodeA"] = &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "foo", - New: "bar", - }, - }, - } - - if diff.Empty() { - t.Fatal("should not be empty") - } -} - -func TestDiffEmpty_taintedIsNotEmpty(t *testing.T) { - diff := new(Diff) - - mod := diff.AddModule(addrs.RootModuleInstance) - mod.Resources["nodeA"] = &InstanceDiff{ - DestroyTainted: true, - } - - if diff.Empty() { - t.Fatal("should not be empty, since DestroyTainted was set") - } -} - -func TestDiffEqual(t *testing.T) { - cases := map[string]struct { - D1, D2 *Diff - Equal bool - }{ - "nil": { - nil, - new(Diff), - false, - }, - - "empty": { - new(Diff), - new(Diff), - true, - }, - - "different module order": { - &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{Path: []string{"root", "foo"}}, - &ModuleDiff{Path: []string{"root", "bar"}}, - }, - }, - &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{Path: []string{"root", "bar"}}, - &ModuleDiff{Path: []string{"root", "foo"}}, - }, - }, - true, - }, - - "different module diff destroys": { - &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{Path: []string{"root", "foo"}, Destroy: true}, - }, - }, - &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{Path: []string{"root", "foo"}, Destroy: false}, - }, - }, - true, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - actual := tc.D1.Equal(tc.D2) - if actual != tc.Equal { - t.Fatalf("expected: %v\n\n%#v\n\n%#v", tc.Equal, tc.D1, tc.D2) - } - }) - } -} - -func TestDiffPrune(t *testing.T) { - cases := map[string]struct { - D1, D2 *Diff - }{ - "nil": { - nil, - nil, - }, - - "empty": { - new(Diff), - new(Diff), - }, - - "empty module": { - &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{Path: []string{"root", "foo"}}, - }, - }, - &Diff{}, - }, - - "destroy module": { - &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{Path: []string{"root", "foo"}, Destroy: true}, - }, - }, - &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{Path: []string{"root", "foo"}, Destroy: true}, - }, - }, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - tc.D1.Prune() - if !tc.D1.Equal(tc.D2) { - t.Fatalf("bad:\n\n%#v\n\n%#v", tc.D1, tc.D2) - } - }) - } -} - -func TestModuleDiff_ChangeType(t *testing.T) { - cases := []struct { - Diff *ModuleDiff - Result DiffChangeType - }{ - { - &ModuleDiff{}, - DiffNone, - }, - { - &ModuleDiff{ - Resources: map[string]*InstanceDiff{ - "foo": &InstanceDiff{Destroy: true}, - }, - }, - DiffDestroy, - }, - { - &ModuleDiff{ - Resources: map[string]*InstanceDiff{ - "foo": &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "", - New: "bar", - }, - }, - }, - }, - }, - DiffUpdate, - }, - { - &ModuleDiff{ - Resources: map[string]*InstanceDiff{ - "foo": &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "", - New: "bar", - RequiresNew: true, - }, - }, - }, - }, - }, - DiffCreate, - }, - { - &ModuleDiff{ - Resources: map[string]*InstanceDiff{ - "foo": &InstanceDiff{ - Destroy: true, - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "", - New: "bar", - RequiresNew: true, - }, - }, - }, - }, - }, - DiffUpdate, - }, - } - - for i, tc := range cases { - actual := tc.Diff.ChangeType() - if actual != tc.Result { - t.Fatalf("%d: %#v", i, actual) - } - } -} - -func TestDiff_DeepCopy(t *testing.T) { - cases := map[string]*Diff{ - "empty": &Diff{}, - - "basic diff": &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{ - Path: []string{"root"}, - Resources: map[string]*InstanceDiff{ - "aws_instance.foo": &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "num": &ResourceAttrDiff{ - Old: "0", - New: "2", - }, - }, - }, - }, - }, - }, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - dup := tc.DeepCopy() - if !reflect.DeepEqual(dup, tc) { - t.Fatalf("\n%#v\n\n%#v", dup, tc) - } - }) - } -} - -func TestModuleDiff_Empty(t *testing.T) { - diff := new(ModuleDiff) - if !diff.Empty() { - t.Fatal("should be empty") - } - - diff.Resources = map[string]*InstanceDiff{ - "nodeA": &InstanceDiff{}, - } - - if !diff.Empty() { - t.Fatal("should be empty") - } - - diff.Resources["nodeA"].Attributes = map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "foo", - New: "bar", - }, - } - - if diff.Empty() { - t.Fatal("should not be empty") - } - - diff.Resources["nodeA"].Attributes = nil - diff.Resources["nodeA"].Destroy = true - - if diff.Empty() { - t.Fatal("should not be empty") - } -} - -func TestModuleDiff_String(t *testing.T) { - diff := &ModuleDiff{ - Resources: map[string]*InstanceDiff{ - "nodeA": &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "foo", - New: "bar", - }, - "bar": &ResourceAttrDiff{ - Old: "foo", - NewComputed: true, - }, - "longfoo": &ResourceAttrDiff{ - Old: "foo", - New: "bar", - RequiresNew: true, - }, - "secretfoo": &ResourceAttrDiff{ - Old: "foo", - New: "bar", - Sensitive: true, - }, - }, - }, - }, - } - - actual := strings.TrimSpace(diff.String()) - expected := strings.TrimSpace(moduleDiffStrBasic) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestInstanceDiff_ChangeType(t *testing.T) { - cases := []struct { - Diff *InstanceDiff - Result DiffChangeType - }{ - { - &InstanceDiff{}, - DiffNone, - }, - { - &InstanceDiff{Destroy: true}, - DiffDestroy, - }, - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "", - New: "bar", - }, - }, - }, - DiffUpdate, - }, - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "", - New: "bar", - RequiresNew: true, - }, - }, - }, - DiffCreate, - }, - { - &InstanceDiff{ - Destroy: true, - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "", - New: "bar", - RequiresNew: true, - }, - }, - }, - DiffDestroyCreate, - }, - { - &InstanceDiff{ - DestroyTainted: true, - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "", - New: "bar", - RequiresNew: true, - }, - }, - }, - DiffDestroyCreate, - }, - } - - for i, tc := range cases { - actual := tc.Diff.ChangeType() - if actual != tc.Result { - t.Fatalf("%d: %#v", i, actual) - } - } -} - -func TestInstanceDiff_Empty(t *testing.T) { - var rd *InstanceDiff - - if !rd.Empty() { - t.Fatal("should be empty") - } - - rd = new(InstanceDiff) - - if !rd.Empty() { - t.Fatal("should be empty") - } - - rd = &InstanceDiff{Destroy: true} - - if rd.Empty() { - t.Fatal("should not be empty") - } - - rd = &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - New: "bar", - }, - }, - } - - if rd.Empty() { - t.Fatal("should not be empty") - } -} - -func TestModuleDiff_Instances(t *testing.T) { - yesDiff := &InstanceDiff{Destroy: true} - noDiff := &InstanceDiff{Destroy: true, DestroyTainted: true} - - cases := []struct { - Diff *ModuleDiff - Id string - Result []*InstanceDiff - }{ - { - &ModuleDiff{ - Resources: map[string]*InstanceDiff{ - "foo": yesDiff, - "bar": noDiff, - }, - }, - "foo", - []*InstanceDiff{ - yesDiff, - }, - }, - - { - &ModuleDiff{ - Resources: map[string]*InstanceDiff{ - "foo": yesDiff, - "foo.0": yesDiff, - "bar": noDiff, - }, - }, - "foo", - []*InstanceDiff{ - yesDiff, - yesDiff, - }, - }, - - { - &ModuleDiff{ - Resources: map[string]*InstanceDiff{ - "foo": yesDiff, - "foo.0": yesDiff, - "foo_bar": noDiff, - "bar": noDiff, - }, - }, - "foo", - []*InstanceDiff{ - yesDiff, - yesDiff, - }, - }, - } - - for i, tc := range cases { - actual := tc.Diff.Instances(tc.Id) - if !reflect.DeepEqual(actual, tc.Result) { - t.Fatalf("%d: %#v", i, actual) - } - } -} - -func TestInstanceDiff_RequiresNew(t *testing.T) { - rd := &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{}, - }, - } - - if rd.RequiresNew() { - t.Fatal("should not require new") - } - - rd.Attributes["foo"].RequiresNew = true - - if !rd.RequiresNew() { - t.Fatal("should require new") - } -} - -func TestInstanceDiff_RequiresNew_nil(t *testing.T) { - var rd *InstanceDiff - - if rd.RequiresNew() { - t.Fatal("should not require new") - } -} - -func TestInstanceDiffSame(t *testing.T) { - cases := []struct { - One, Two *InstanceDiff - Same bool - Reason string - }{ - { - &InstanceDiff{}, - &InstanceDiff{}, - true, - "", - }, - - { - nil, - nil, - true, - "", - }, - - { - &InstanceDiff{Destroy: false}, - &InstanceDiff{Destroy: true}, - false, - "diff: Destroy; old: false, new: true", - }, - - { - &InstanceDiff{Destroy: true}, - &InstanceDiff{Destroy: true}, - true, - "", - }, - - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{}, - }, - }, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{}, - }, - }, - true, - "", - }, - - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "bar": &ResourceAttrDiff{}, - }, - }, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{}, - }, - }, - false, - "attribute mismatch: bar", - }, - - // Extra attributes - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{}, - }, - }, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{}, - "bar": &ResourceAttrDiff{}, - }, - }, - false, - "extra attributes: bar", - }, - - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{RequiresNew: true}, - }, - }, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{RequiresNew: false}, - }, - }, - false, - "diff RequiresNew; old: true, new: false", - }, - - // NewComputed on primitive - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "", - New: "${var.foo}", - NewComputed: true, - }, - }, - }, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "0", - New: "1", - }, - }, - }, - true, - "", - }, - - // NewComputed on primitive, removed - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "", - New: "${var.foo}", - NewComputed: true, - }, - }, - }, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{}, - }, - true, - "", - }, - - // NewComputed on set, removed - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo.#": &ResourceAttrDiff{ - Old: "", - New: "", - NewComputed: true, - }, - }, - }, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo.1": &ResourceAttrDiff{ - Old: "foo", - New: "", - NewRemoved: true, - }, - "foo.2": &ResourceAttrDiff{ - Old: "", - New: "bar", - }, - }, - }, - true, - "", - }, - - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo.#": &ResourceAttrDiff{NewComputed: true}, - }, - }, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo.#": &ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "foo.0": &ResourceAttrDiff{ - Old: "", - New: "12", - }, - }, - }, - true, - "", - }, - - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo.#": &ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "foo.~35964334.bar": &ResourceAttrDiff{ - Old: "", - New: "${var.foo}", - }, - }, - }, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo.#": &ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "foo.87654323.bar": &ResourceAttrDiff{ - Old: "", - New: "12", - }, - }, - }, - true, - "", - }, - - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo.#": &ResourceAttrDiff{ - Old: "0", - NewComputed: true, - }, - }, - }, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{}, - }, - true, - "", - }, - - // Computed can change RequiresNew by removal, and that's okay - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo.#": &ResourceAttrDiff{ - Old: "0", - NewComputed: true, - RequiresNew: true, - }, - }, - }, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{}, - }, - true, - "", - }, - - // Computed can change Destroy by removal, and that's okay - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo.#": &ResourceAttrDiff{ - Old: "0", - NewComputed: true, - RequiresNew: true, - }, - }, - - Destroy: true, - }, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{}, - }, - true, - "", - }, - - // Computed can change Destroy by elements - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo.#": &ResourceAttrDiff{ - Old: "0", - NewComputed: true, - RequiresNew: true, - }, - }, - - Destroy: true, - }, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo.#": &ResourceAttrDiff{ - Old: "1", - New: "1", - }, - "foo.12": &ResourceAttrDiff{ - Old: "4", - New: "12", - RequiresNew: true, - }, - }, - - Destroy: true, - }, - true, - "", - }, - - // Computed sets may not contain all fields in the original diff, and - // because multiple entries for the same set can compute to the same - // hash before the values are computed or interpolated, the overall - // count can change as well. - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo.#": &ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "foo.~35964334.bar": &ResourceAttrDiff{ - Old: "", - New: "${var.foo}", - }, - }, - }, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo.#": &ResourceAttrDiff{ - Old: "0", - New: "2", - }, - "foo.87654323.bar": &ResourceAttrDiff{ - Old: "", - New: "12", - }, - "foo.87654325.bar": &ResourceAttrDiff{ - Old: "", - New: "12", - }, - "foo.87654325.baz": &ResourceAttrDiff{ - Old: "", - New: "12", - }, - }, - }, - true, - "", - }, - - // Computed values in maps will fail the "Same" check as well - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo.%": &ResourceAttrDiff{ - Old: "", - New: "", - NewComputed: true, - }, - }, - }, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo.%": &ResourceAttrDiff{ - Old: "0", - New: "1", - NewComputed: false, - }, - "foo.val": &ResourceAttrDiff{ - Old: "", - New: "something", - }, - }, - }, - true, - "", - }, - - // In a DESTROY/CREATE scenario, the plan diff will be run against the - // state of the old instance, while the apply diff will be run against an - // empty state (because the state is cleared when the destroy runs.) - // For complex attributes, this can result in keys that seem to disappear - // between the two diffs, when in reality everything is working just fine. - // - // Same() needs to take into account this scenario by analyzing NewRemoved - // and treating as "Same" a diff that does indeed have that key removed. - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "somemap.oldkey": &ResourceAttrDiff{ - Old: "long ago", - New: "", - NewRemoved: true, - }, - "somemap.newkey": &ResourceAttrDiff{ - Old: "", - New: "brave new world", - }, - }, - }, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "somemap.newkey": &ResourceAttrDiff{ - Old: "", - New: "brave new world", - }, - }, - }, - true, - "", - }, - - // Another thing that can occur in DESTROY/CREATE scenarios is that list - // values that are going to zero have diffs that show up at plan time but - // are gone at apply time. The NewRemoved handling catches the fields and - // treats them as OK, but it also needs to treat the .# field itself as - // okay to be present in the old diff but not in the new one. - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "reqnew": &ResourceAttrDiff{ - Old: "old", - New: "new", - RequiresNew: true, - }, - "somemap.#": &ResourceAttrDiff{ - Old: "1", - New: "0", - }, - "somemap.oldkey": &ResourceAttrDiff{ - Old: "long ago", - New: "", - NewRemoved: true, - }, - }, - }, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "reqnew": &ResourceAttrDiff{ - Old: "", - New: "new", - RequiresNew: true, - }, - }, - }, - true, - "", - }, - - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "reqnew": &ResourceAttrDiff{ - Old: "old", - New: "new", - RequiresNew: true, - }, - "somemap.%": &ResourceAttrDiff{ - Old: "1", - New: "0", - }, - "somemap.oldkey": &ResourceAttrDiff{ - Old: "long ago", - New: "", - NewRemoved: true, - }, - }, - }, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "reqnew": &ResourceAttrDiff{ - Old: "", - New: "new", - RequiresNew: true, - }, - }, - }, - true, - "", - }, - - // Innner computed set should allow outer change in key - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo.#": &ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "foo.~1.outer_val": &ResourceAttrDiff{ - Old: "", - New: "foo", - }, - "foo.~1.inner.#": &ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "foo.~1.inner.~2.value": &ResourceAttrDiff{ - Old: "", - New: "${var.bar}", - NewComputed: true, - }, - }, - }, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo.#": &ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "foo.12.outer_val": &ResourceAttrDiff{ - Old: "", - New: "foo", - }, - "foo.12.inner.#": &ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "foo.12.inner.42.value": &ResourceAttrDiff{ - Old: "", - New: "baz", - }, - }, - }, - true, - "", - }, - - // Innner computed list should allow outer change in key - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo.#": &ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "foo.~1.outer_val": &ResourceAttrDiff{ - Old: "", - New: "foo", - }, - "foo.~1.inner.#": &ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "foo.~1.inner.0.value": &ResourceAttrDiff{ - Old: "", - New: "${var.bar}", - NewComputed: true, - }, - }, - }, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo.#": &ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "foo.12.outer_val": &ResourceAttrDiff{ - Old: "", - New: "foo", - }, - "foo.12.inner.#": &ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "foo.12.inner.0.value": &ResourceAttrDiff{ - Old: "", - New: "baz", - }, - }, - }, - true, - "", - }, - - // When removing all collection items, the diff is allowed to contain - // nothing when re-creating the resource. This should be the "Same" - // since we said we were going from 1 to 0. - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo.%": &ResourceAttrDiff{ - Old: "1", - New: "0", - RequiresNew: true, - }, - "foo.bar": &ResourceAttrDiff{ - Old: "baz", - New: "", - NewRemoved: true, - RequiresNew: true, - }, - }, - }, - &InstanceDiff{}, - true, - "", - }, - - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo.#": &ResourceAttrDiff{ - Old: "1", - New: "0", - RequiresNew: true, - }, - "foo.0": &ResourceAttrDiff{ - Old: "baz", - New: "", - NewRemoved: true, - RequiresNew: true, - }, - }, - }, - &InstanceDiff{}, - true, - "", - }, - - // Make sure that DestroyTainted diffs pass as well, especially when diff - // two works off of no state. - { - &InstanceDiff{ - DestroyTainted: true, - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "foo", - New: "foo", - }, - }, - }, - &InstanceDiff{ - DestroyTainted: true, - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "", - New: "foo", - }, - }, - }, - true, - "", - }, - // RequiresNew in different attribute - { - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "foo", - New: "foo", - }, - "bar": &ResourceAttrDiff{ - Old: "bar", - New: "baz", - RequiresNew: true, - }, - }, - }, - &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "", - New: "foo", - }, - "bar": &ResourceAttrDiff{ - Old: "", - New: "baz", - RequiresNew: true, - }, - }, - }, - true, - "", - }, - } - - for i, tc := range cases { - t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { - same, reason := tc.One.Same(tc.Two) - if same != tc.Same { - t.Fatalf("%d: expected same: %t, got %t (%s)\n\n one: %#v\n\ntwo: %#v", - i, tc.Same, same, reason, tc.One, tc.Two) - } - if reason != tc.Reason { - t.Fatalf( - "%d: bad reason\n\nexpected: %#v\n\ngot: %#v", i, tc.Reason, reason) - } - }) - } -} - -const moduleDiffStrBasic = ` -CREATE: nodeA - bar: "foo" => "" - foo: "foo" => "bar" - longfoo: "foo" => "bar" (forces new resource) - secretfoo: "" => "" (attribute changed) -` - -func TestCountFlatmapContainerValues(t *testing.T) { - for i, tc := range []struct { - attrs map[string]string - key string - count string - }{ - { - attrs: map[string]string{"set.2.list.#": "9999", "set.2.list.0": "x", "set.2.list.0.z": "y", "set.2.attr": "bar", "set.#": "9999"}, - key: "set.2.list.#", - count: "1", - }, - { - attrs: map[string]string{"set.2.list.#": "9999", "set.2.list.0": "x", "set.2.list.0.z": "y", "set.2.attr": "bar", "set.#": "9999"}, - key: "set.#", - count: "1", - }, - { - attrs: map[string]string{"set.2.list.0": "x", "set.2.list.0.z": "y", "set.2.attr": "bar", "set.#": "9999"}, - key: "set.#", - count: "1", - }, - { - attrs: map[string]string{"map.#": "3", "map.a": "b", "map.a.#": "0", "map.b": "4"}, - key: "map.#", - count: "2", - }, - } { - t.Run(strconv.Itoa(i), func(t *testing.T) { - count := countFlatmapContainerValues(tc.key, tc.attrs) - if count != tc.count { - t.Fatalf("expected %q, got %q", tc.count, count) - } - }) - } -}