package schema import ( "errors" "fmt" "reflect" "strings" "sync" "github.com/hashicorp/terraform/terraform" ) // newValueWriter is a minor re-implementation of MapFieldWriter to include // keys that should be marked as computed, to represent the new part of a // pseudo-diff. type newValueWriter struct { *MapFieldWriter // A list of keys that should be marked as computed. computedKeys map[string]bool // A lock to prevent races on writes. The underlying writer will have one as // well - this is for computed keys. lock sync.Mutex // To be used with init. once sync.Once } // init performs any initialization tasks for the newValueWriter. func (w *newValueWriter) init() { if w.computedKeys == nil { w.computedKeys = make(map[string]bool) } } // WriteField overrides MapValueWriter's WriteField, adding the ability to flag // the address as computed. func (w *newValueWriter) WriteField(address []string, value interface{}, computed bool) error { // Fail the write if we have a non-nil value and computed is true. // NewComputed values should not have a value when written. if value != nil && computed { return errors.New("Non-nil value with computed set") } if err := w.MapFieldWriter.WriteField(address, value); err != nil { return err } w.once.Do(w.init) w.lock.Lock() defer w.lock.Unlock() if computed { w.computedKeys[strings.Join(address, ".")] = true } return nil } // ComputedKeysMap returns the underlying computed keys map. func (w *newValueWriter) ComputedKeysMap() map[string]bool { w.once.Do(w.init) return w.computedKeys } // newValueReader is a minor re-implementation of MapFieldReader and is the // read counterpart to MapValueWriter, allowing the read of keys flagged as // computed to accommodate the diff override logic in ResourceDiff. type newValueReader struct { *MapFieldReader // The list of computed keys from a newValueWriter. computedKeys map[string]bool } // ReadField reads the values from the underlying writer, returning the // computed value if it is found as well. func (r *newValueReader) ReadField(address []string) (FieldReadResult, error) { addrKey := strings.Join(address, ".") v, err := r.MapFieldReader.ReadField(address) if err != nil { return FieldReadResult{}, err } for computedKey := range r.computedKeys { if childAddrOf(addrKey, computedKey) { if strings.HasSuffix(addrKey, ".#") { // This is a count value for a list or set that has been marked as // computed, or a sub-list/sub-set of a complex resource that has // been marked as computed. We need to pass through to other readers // so that an accurate previous count can be fetched for the diff. v.Exists = false } v.Computed = true } } return v, nil } // ResourceDiff is used to query and make custom changes to an in-flight diff. // It can be used to veto particular changes in the diff, customize the diff // that has been created, or diff values not controlled by config. // // The object functions similar to ResourceData, however most notably lacks // Set, SetPartial, and Partial, as it should be used to change diff values // only. Most other first-class ResourceData functions exist, namely Get, // GetOk, HasChange, and GetChange exist. // // All functions in ResourceDiff, save for ForceNew, can only be used on // computed fields. type ResourceDiff struct { // The schema for the resource being worked on. schema map[string]*Schema // The current config for this resource. config *terraform.ResourceConfig // The state for this resource as it exists post-refresh, after the initial // diff. state *terraform.InstanceState // The diff created by Terraform. This diff is used, along with state, // config, and custom-set diff data, to provide a multi-level reader // experience similar to ResourceData. diff *terraform.InstanceDiff // The internal reader structure that contains the state, config, the default // diff, and the new diff. multiReader *MultiLevelFieldReader // A writer that writes overridden new fields. newWriter *newValueWriter // Tracks which keys have been updated by ResourceDiff to ensure that the // diff does not get re-run on keys that were not touched, or diffs that were // just removed (re-running on the latter would just roll back the removal). updatedKeys map[string]bool // Tracks which keys were flagged as forceNew. These keys are not saved in // newWriter, but we need to track them so that they can be re-diffed later. forcedNewKeys map[string]bool } // newResourceDiff creates a new ResourceDiff instance. func newResourceDiff(schema map[string]*Schema, config *terraform.ResourceConfig, state *terraform.InstanceState, diff *terraform.InstanceDiff) *ResourceDiff { d := &ResourceDiff{ config: config, state: state, diff: diff, schema: schema, } d.newWriter = &newValueWriter{ MapFieldWriter: &MapFieldWriter{Schema: d.schema}, } readers := make(map[string]FieldReader) var stateAttributes map[string]string if d.state != nil { stateAttributes = d.state.Attributes readers["state"] = &MapFieldReader{ Schema: d.schema, Map: BasicMapReader(stateAttributes), } } if d.config != nil { readers["config"] = &ConfigFieldReader{ Schema: d.schema, Config: d.config, } } if d.diff != nil { readers["diff"] = &DiffFieldReader{ Schema: d.schema, Diff: d.diff, Source: &MultiLevelFieldReader{ Levels: []string{"state", "config"}, Readers: readers, }, } } readers["newDiff"] = &newValueReader{ MapFieldReader: &MapFieldReader{ Schema: d.schema, Map: BasicMapReader(d.newWriter.Map()), }, computedKeys: d.newWriter.ComputedKeysMap(), } d.multiReader = &MultiLevelFieldReader{ Levels: []string{ "state", "config", "diff", "newDiff", }, Readers: readers, } d.updatedKeys = make(map[string]bool) d.forcedNewKeys = make(map[string]bool) return d } // UpdatedKeys returns the keys that were updated by this ResourceDiff run. // These are the only keys that a diff should be re-calculated for. // // This is the combined result of both keys for which diff values were updated // for or cleared, and also keys that were flagged to be re-diffed as a result // of ForceNew. func (d *ResourceDiff) UpdatedKeys() []string { var s []string for k := range d.updatedKeys { s = append(s, k) } for k := range d.forcedNewKeys { for _, l := range s { if k == l { break } } s = append(s, k) } return s } // Clear wipes the diff for a particular key. It is called by ResourceDiff's // functionality to remove any possibility of conflicts, but can be called on // its own to just remove a specific key from the diff completely. // // Note that this does not wipe an override. This function is only allowed on // computed keys. func (d *ResourceDiff) Clear(key string) error { if err := d.checkKey(key, "Clear", true); err != nil { return err } return d.clear(key) } func (d *ResourceDiff) clear(key string) error { // Check the schema to make sure that this key exists first. schemaL := addrToSchema(strings.Split(key, "."), d.schema) if len(schemaL) == 0 { return fmt.Errorf("%s is not a valid key", key) } for k := range d.diff.Attributes { if strings.HasPrefix(k, key) { delete(d.diff.Attributes, k) } } return nil } // GetChangedKeysPrefix helps to implement Resource.CustomizeDiff // where we need to act on all nested fields // without calling out each one separately func (d *ResourceDiff) GetChangedKeysPrefix(prefix string) []string { keys := make([]string, 0) for k := range d.diff.Attributes { if strings.HasPrefix(k, prefix) { keys = append(keys, k) } } return keys } // diffChange helps to implement resourceDiffer and derives its change values // from ResourceDiff's own change data, in addition to existing diff, config, and state. func (d *ResourceDiff) diffChange(key string) (interface{}, interface{}, bool, bool, bool) { old, new, customized := d.getChange(key) if !old.Exists { old.Value = nil } if !new.Exists || d.removed(key) { new.Value = nil } return old.Value, new.Value, !reflect.DeepEqual(old.Value, new.Value), new.Computed, customized } // SetNew is used to set a new diff value for the mentioned key. The value must // be correct for the attribute's schema (mostly relevant for maps, lists, and // sets). The original value from the state is used as the old value. // // This function is only allowed on computed attributes. func (d *ResourceDiff) SetNew(key string, value interface{}) error { if err := d.checkKey(key, "SetNew", false); err != nil { return err } return d.setDiff(key, value, false) } // SetNewComputed functions like SetNew, except that it blanks out a new value // and marks it as computed. // // This function is only allowed on computed attributes. func (d *ResourceDiff) SetNewComputed(key string) error { if err := d.checkKey(key, "SetNewComputed", false); err != nil { return err } return d.setDiff(key, nil, true) } // setDiff performs common diff setting behaviour. func (d *ResourceDiff) setDiff(key string, new interface{}, computed bool) error { if err := d.clear(key); err != nil { return err } if err := d.newWriter.WriteField(strings.Split(key, "."), new, computed); err != nil { return fmt.Errorf("Cannot set new diff value for key %s: %s", key, err) } d.updatedKeys[key] = true return nil } // ForceNew force-flags ForceNew in the schema for a specific key, and // re-calculates its diff, effectively causing this attribute to force a new // resource. // // Keep in mind that forcing a new resource will force a second run of the // resource's CustomizeDiff function (with a new ResourceDiff) once the current // one has completed. This second run is performed without state. This behavior // will be the same as if a new resource is being created and is performed to // ensure that the diff looks like the diff for a new resource as much as // possible. CustomizeDiff should expect such a scenario and act correctly. // // This function is a no-op/error if there is no diff. // // Note that the change to schema is permanent for the lifecycle of this // specific ResourceDiff instance. func (d *ResourceDiff) ForceNew(key string) error { if !d.HasChange(key) { return fmt.Errorf("ForceNew: No changes for %s", key) } keyParts := strings.Split(key, ".") var schema *Schema schemaL := addrToSchema(keyParts, d.schema) if len(schemaL) > 0 { schema = schemaL[len(schemaL)-1] } else { return fmt.Errorf("ForceNew: %s is not a valid key", key) } schema.ForceNew = true // Flag this for a re-diff. Don't save any values to guarantee that existing // diffs aren't messed with, as this gets messy when dealing with complex // structures, zero values, etc. d.forcedNewKeys[keyParts[0]] = true return nil } // Get hands off to ResourceData.Get. func (d *ResourceDiff) Get(key string) interface{} { r, _ := d.GetOk(key) return r } // GetChange gets the change between the state and diff, checking first to see // if an overridden diff exists. // // This implementation differs from ResourceData's in the way that we first get // results from the exact levels for the new diff, then from state and diff as // per normal. func (d *ResourceDiff) GetChange(key string) (interface{}, interface{}) { old, new, _ := d.getChange(key) return old.Value, new.Value } // GetOk functions the same way as ResourceData.GetOk, but it also checks the // new diff levels to provide data consistent with the current state of the // customized diff. func (d *ResourceDiff) GetOk(key string) (interface{}, bool) { r := d.get(strings.Split(key, "."), "newDiff") exists := r.Exists && !r.Computed if exists { // If it exists, we also want to verify it is not the zero-value. value := r.Value zero := r.Schema.Type.Zero() if eq, ok := value.(Equal); ok { exists = !eq.Equal(zero) } else { exists = !reflect.DeepEqual(value, zero) } } return r.Value, exists } // GetOkExists functions the same way as GetOkExists within ResourceData, but // it also checks the new diff levels to provide data consistent with the // current state of the customized diff. // // This is nearly the same function as GetOk, yet it does not check // for the zero value of the attribute's type. This allows for attributes // without a default, to fully check for a literal assignment, regardless // of the zero-value for that type. func (d *ResourceDiff) GetOkExists(key string) (interface{}, bool) { r := d.get(strings.Split(key, "."), "newDiff") exists := r.Exists && !r.Computed return r.Value, exists } // NewValueKnown returns true if the new value for the given key is available // as its final value at diff time. If the return value is false, this means // either the value is based of interpolation that was unavailable at diff // time, or that the value was explicitly marked as computed by SetNewComputed. func (d *ResourceDiff) NewValueKnown(key string) bool { r := d.get(strings.Split(key, "."), "newDiff") return !r.Computed } // HasChange checks to see if there is a change between state and the diff, or // in the overridden diff. func (d *ResourceDiff) HasChange(key string) bool { old, new := d.GetChange(key) // If the type implements the Equal interface, then call that // instead of just doing a reflect.DeepEqual. An example where this is // needed is *Set if eq, ok := old.(Equal); ok { return !eq.Equal(new) } return !reflect.DeepEqual(old, new) } // Id returns the ID of this resource. // // Note that technically, ID does not change during diffs (it either has // already changed in the refresh, or will change on update), hence we do not // support updating the ID or fetching it from anything else other than state. func (d *ResourceDiff) Id() string { var result string if d.state != nil { result = d.state.ID } return result } // getChange gets values from two different levels, designed for use in // diffChange, HasChange, and GetChange. // // This implementation differs from ResourceData's in the way that we first get // results from the exact levels for the new diff, then from state and diff as // per normal. func (d *ResourceDiff) getChange(key string) (getResult, getResult, bool) { old := d.get(strings.Split(key, "."), "state") var new getResult for p := range d.updatedKeys { if childAddrOf(key, p) { new = d.getExact(strings.Split(key, "."), "newDiff") return old, new, true } } new = d.get(strings.Split(key, "."), "newDiff") return old, new, false } // removed checks to see if the key is present in the existing, pre-customized // diff and if it was marked as NewRemoved. func (d *ResourceDiff) removed(k string) bool { diff, ok := d.diff.Attributes[k] if !ok { return false } return diff.NewRemoved } // get performs the appropriate multi-level reader logic for ResourceDiff, // starting at source. Refer to newResourceDiff for the level order. func (d *ResourceDiff) get(addr []string, source string) getResult { result, err := d.multiReader.ReadFieldMerge(addr, source) if err != nil { panic(err) } return d.finalizeResult(addr, result) } // getExact gets an attribute from the exact level referenced by source. func (d *ResourceDiff) getExact(addr []string, source string) getResult { result, err := d.multiReader.ReadFieldExact(addr, source) if err != nil { panic(err) } return d.finalizeResult(addr, result) } // finalizeResult does some post-processing of the result produced by get and getExact. func (d *ResourceDiff) finalizeResult(addr []string, result FieldReadResult) getResult { // If the result doesn't exist, then we set the value to the zero value var schema *Schema if schemaL := addrToSchema(addr, d.schema); len(schemaL) > 0 { schema = schemaL[len(schemaL)-1] } if result.Value == nil && schema != nil { result.Value = result.ValueOrZero(schema) } // Transform the FieldReadResult into a getResult. It might be worth // merging these two structures one day. return getResult{ Value: result.Value, ValueProcessed: result.ValueProcessed, Computed: result.Computed, Exists: result.Exists, Schema: schema, } } // childAddrOf does a comparison of two addresses to see if one is the child of // the other. func childAddrOf(child, parent string) bool { cs := strings.Split(child, ".") ps := strings.Split(parent, ".") if len(ps) > len(cs) { return false } return reflect.DeepEqual(ps, cs[:len(ps)]) } // checkKey checks the key to make sure it exists and is computed. func (d *ResourceDiff) checkKey(key, caller string, nested bool) error { var schema *Schema if nested { keyParts := strings.Split(key, ".") schemaL := addrToSchema(keyParts, d.schema) if len(schemaL) > 0 { schema = schemaL[len(schemaL)-1] } } else { s, ok := d.schema[key] if ok { schema = s } } if schema == nil { return fmt.Errorf("%s: invalid key: %s", caller, key) } if !schema.Computed { return fmt.Errorf("%s only operates on computed keys - %s is not one", caller, key) } return nil }