| package terraform |
| |
| import ( |
| "bufio" |
| "bytes" |
| "fmt" |
| "log" |
| "reflect" |
| "regexp" |
| "sort" |
| "strconv" |
| "strings" |
| "sync" |
| |
| "github.com/hashicorp/terraform/internal/addrs" |
| "github.com/hashicorp/terraform/internal/configs/configschema" |
| "github.com/hashicorp/terraform/internal/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 { |
| 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{} |
| } |
| |
| // 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, ".") |
| } |