mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-27 09:21:14 -06:00
560 lines
17 KiB
Go
560 lines
17 KiB
Go
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
|
|
}
|