github.com/opentofu/opentofu@v1.7.1/internal/legacy/helper/schema/resource_diff.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package schema
     7  
     8  import (
     9  	"errors"
    10  	"fmt"
    11  	"reflect"
    12  	"strings"
    13  	"sync"
    14  
    15  	"github.com/opentofu/opentofu/internal/legacy/tofu"
    16  )
    17  
    18  // newValueWriter is a minor re-implementation of MapFieldWriter to include
    19  // keys that should be marked as computed, to represent the new part of a
    20  // pseudo-diff.
    21  type newValueWriter struct {
    22  	*MapFieldWriter
    23  
    24  	// A list of keys that should be marked as computed.
    25  	computedKeys map[string]bool
    26  
    27  	// A lock to prevent races on writes. The underlying writer will have one as
    28  	// well - this is for computed keys.
    29  	lock sync.Mutex
    30  
    31  	// To be used with init.
    32  	once sync.Once
    33  }
    34  
    35  // init performs any initialization tasks for the newValueWriter.
    36  func (w *newValueWriter) init() {
    37  	if w.computedKeys == nil {
    38  		w.computedKeys = make(map[string]bool)
    39  	}
    40  }
    41  
    42  // WriteField overrides MapValueWriter's WriteField, adding the ability to flag
    43  // the address as computed.
    44  func (w *newValueWriter) WriteField(address []string, value interface{}, computed bool) error {
    45  	// Fail the write if we have a non-nil value and computed is true.
    46  	// NewComputed values should not have a value when written.
    47  	if value != nil && computed {
    48  		return errors.New("Non-nil value with computed set")
    49  	}
    50  
    51  	if err := w.MapFieldWriter.WriteField(address, value); err != nil {
    52  		return err
    53  	}
    54  
    55  	w.once.Do(w.init)
    56  
    57  	w.lock.Lock()
    58  	defer w.lock.Unlock()
    59  	if computed {
    60  		w.computedKeys[strings.Join(address, ".")] = true
    61  	}
    62  	return nil
    63  }
    64  
    65  // ComputedKeysMap returns the underlying computed keys map.
    66  func (w *newValueWriter) ComputedKeysMap() map[string]bool {
    67  	w.once.Do(w.init)
    68  	return w.computedKeys
    69  }
    70  
    71  // newValueReader is a minor re-implementation of MapFieldReader and is the
    72  // read counterpart to MapValueWriter, allowing the read of keys flagged as
    73  // computed to accommodate the diff override logic in ResourceDiff.
    74  type newValueReader struct {
    75  	*MapFieldReader
    76  
    77  	// The list of computed keys from a newValueWriter.
    78  	computedKeys map[string]bool
    79  }
    80  
    81  // ReadField reads the values from the underlying writer, returning the
    82  // computed value if it is found as well.
    83  func (r *newValueReader) ReadField(address []string) (FieldReadResult, error) {
    84  	addrKey := strings.Join(address, ".")
    85  	v, err := r.MapFieldReader.ReadField(address)
    86  	if err != nil {
    87  		return FieldReadResult{}, err
    88  	}
    89  	for computedKey := range r.computedKeys {
    90  		if childAddrOf(addrKey, computedKey) {
    91  			if strings.HasSuffix(addrKey, ".#") {
    92  				// This is a count value for a list or set that has been marked as
    93  				// computed, or a sub-list/sub-set of a complex resource that has
    94  				// been marked as computed.  We need to pass through to other readers
    95  				// so that an accurate previous count can be fetched for the diff.
    96  				v.Exists = false
    97  			}
    98  			v.Computed = true
    99  		}
   100  	}
   101  
   102  	return v, nil
   103  }
   104  
   105  // ResourceDiff is used to query and make custom changes to an in-flight diff.
   106  // It can be used to veto particular changes in the diff, customize the diff
   107  // that has been created, or diff values not controlled by config.
   108  //
   109  // The object functions similar to ResourceData, however most notably lacks
   110  // Set, SetPartial, and Partial, as it should be used to change diff values
   111  // only.  Most other first-class ResourceData functions exist, namely Get,
   112  // GetOk, HasChange, and GetChange exist.
   113  //
   114  // All functions in ResourceDiff, save for ForceNew, can only be used on
   115  // computed fields.
   116  type ResourceDiff struct {
   117  	// The schema for the resource being worked on.
   118  	schema map[string]*Schema
   119  
   120  	// The current config for this resource.
   121  	config *tofu.ResourceConfig
   122  
   123  	// The state for this resource as it exists post-refresh, after the initial
   124  	// diff.
   125  	state *tofu.InstanceState
   126  
   127  	// The diff created by Tofu. This diff is used, along with state,
   128  	// config, and custom-set diff data, to provide a multi-level reader
   129  	// experience similar to ResourceData.
   130  	diff *tofu.InstanceDiff
   131  
   132  	// The internal reader structure that contains the state, config, the default
   133  	// diff, and the new diff.
   134  	multiReader *MultiLevelFieldReader
   135  
   136  	// A writer that writes overridden new fields.
   137  	newWriter *newValueWriter
   138  
   139  	// Tracks which keys have been updated by ResourceDiff to ensure that the
   140  	// diff does not get re-run on keys that were not touched, or diffs that were
   141  	// just removed (re-running on the latter would just roll back the removal).
   142  	updatedKeys map[string]bool
   143  
   144  	// Tracks which keys were flagged as forceNew. These keys are not saved in
   145  	// newWriter, but we need to track them so that they can be re-diffed later.
   146  	forcedNewKeys map[string]bool
   147  }
   148  
   149  // newResourceDiff creates a new ResourceDiff instance.
   150  func newResourceDiff(schema map[string]*Schema, config *tofu.ResourceConfig, state *tofu.InstanceState, diff *tofu.InstanceDiff) *ResourceDiff {
   151  	d := &ResourceDiff{
   152  		config: config,
   153  		state:  state,
   154  		diff:   diff,
   155  		schema: schema,
   156  	}
   157  
   158  	d.newWriter = &newValueWriter{
   159  		MapFieldWriter: &MapFieldWriter{Schema: d.schema},
   160  	}
   161  	readers := make(map[string]FieldReader)
   162  	var stateAttributes map[string]string
   163  	if d.state != nil {
   164  		stateAttributes = d.state.Attributes
   165  		readers["state"] = &MapFieldReader{
   166  			Schema: d.schema,
   167  			Map:    BasicMapReader(stateAttributes),
   168  		}
   169  	}
   170  	if d.config != nil {
   171  		readers["config"] = &ConfigFieldReader{
   172  			Schema: d.schema,
   173  			Config: d.config,
   174  		}
   175  	}
   176  	if d.diff != nil {
   177  		readers["diff"] = &DiffFieldReader{
   178  			Schema: d.schema,
   179  			Diff:   d.diff,
   180  			Source: &MultiLevelFieldReader{
   181  				Levels:  []string{"state", "config"},
   182  				Readers: readers,
   183  			},
   184  		}
   185  	}
   186  	readers["newDiff"] = &newValueReader{
   187  		MapFieldReader: &MapFieldReader{
   188  			Schema: d.schema,
   189  			Map:    BasicMapReader(d.newWriter.Map()),
   190  		},
   191  		computedKeys: d.newWriter.ComputedKeysMap(),
   192  	}
   193  	d.multiReader = &MultiLevelFieldReader{
   194  		Levels: []string{
   195  			"state",
   196  			"config",
   197  			"diff",
   198  			"newDiff",
   199  		},
   200  
   201  		Readers: readers,
   202  	}
   203  
   204  	d.updatedKeys = make(map[string]bool)
   205  	d.forcedNewKeys = make(map[string]bool)
   206  
   207  	return d
   208  }
   209  
   210  // UpdatedKeys returns the keys that were updated by this ResourceDiff run.
   211  // These are the only keys that a diff should be re-calculated for.
   212  //
   213  // This is the combined result of both keys for which diff values were updated
   214  // for or cleared, and also keys that were flagged to be re-diffed as a result
   215  // of ForceNew.
   216  func (d *ResourceDiff) UpdatedKeys() []string {
   217  	var s []string
   218  	for k := range d.updatedKeys {
   219  		s = append(s, k)
   220  	}
   221  	for k := range d.forcedNewKeys {
   222  		for _, l := range s {
   223  			if k == l {
   224  				break
   225  			}
   226  		}
   227  		s = append(s, k)
   228  	}
   229  	return s
   230  }
   231  
   232  // Clear wipes the diff for a particular key. It is called by ResourceDiff's
   233  // functionality to remove any possibility of conflicts, but can be called on
   234  // its own to just remove a specific key from the diff completely.
   235  //
   236  // Note that this does not wipe an override. This function is only allowed on
   237  // computed keys.
   238  func (d *ResourceDiff) Clear(key string) error {
   239  	if err := d.checkKey(key, "Clear", true); err != nil {
   240  		return err
   241  	}
   242  
   243  	return d.clear(key)
   244  }
   245  
   246  func (d *ResourceDiff) clear(key string) error {
   247  	// Check the schema to make sure that this key exists first.
   248  	schemaL := addrToSchema(strings.Split(key, "."), d.schema)
   249  	if len(schemaL) == 0 {
   250  		return fmt.Errorf("%s is not a valid key", key)
   251  	}
   252  
   253  	for k := range d.diff.Attributes {
   254  		if strings.HasPrefix(k, key) {
   255  			delete(d.diff.Attributes, k)
   256  		}
   257  	}
   258  	return nil
   259  }
   260  
   261  // GetChangedKeysPrefix helps to implement Resource.CustomizeDiff
   262  // where we need to act on all nested fields
   263  // without calling out each one separately
   264  func (d *ResourceDiff) GetChangedKeysPrefix(prefix string) []string {
   265  	keys := make([]string, 0)
   266  	for k := range d.diff.Attributes {
   267  		if strings.HasPrefix(k, prefix) {
   268  			keys = append(keys, k)
   269  		}
   270  	}
   271  	return keys
   272  }
   273  
   274  // diffChange helps to implement resourceDiffer and derives its change values
   275  // from ResourceDiff's own change data, in addition to existing diff, config, and state.
   276  func (d *ResourceDiff) diffChange(key string) (interface{}, interface{}, bool, bool, bool) {
   277  	old, new, customized := d.getChange(key)
   278  
   279  	if !old.Exists {
   280  		old.Value = nil
   281  	}
   282  	if !new.Exists || d.removed(key) {
   283  		new.Value = nil
   284  	}
   285  
   286  	return old.Value, new.Value, !reflect.DeepEqual(old.Value, new.Value), new.Computed, customized
   287  }
   288  
   289  // SetNew is used to set a new diff value for the mentioned key. The value must
   290  // be correct for the attribute's schema (mostly relevant for maps, lists, and
   291  // sets). The original value from the state is used as the old value.
   292  //
   293  // This function is only allowed on computed attributes.
   294  func (d *ResourceDiff) SetNew(key string, value interface{}) error {
   295  	if err := d.checkKey(key, "SetNew", false); err != nil {
   296  		return err
   297  	}
   298  
   299  	return d.setDiff(key, value, false)
   300  }
   301  
   302  // SetNewComputed functions like SetNew, except that it blanks out a new value
   303  // and marks it as computed.
   304  //
   305  // This function is only allowed on computed attributes.
   306  func (d *ResourceDiff) SetNewComputed(key string) error {
   307  	if err := d.checkKey(key, "SetNewComputed", false); err != nil {
   308  		return err
   309  	}
   310  
   311  	return d.setDiff(key, nil, true)
   312  }
   313  
   314  // setDiff performs common diff setting behaviour.
   315  func (d *ResourceDiff) setDiff(key string, new interface{}, computed bool) error {
   316  	if err := d.clear(key); err != nil {
   317  		return err
   318  	}
   319  
   320  	if err := d.newWriter.WriteField(strings.Split(key, "."), new, computed); err != nil {
   321  		return fmt.Errorf("Cannot set new diff value for key %s: %w", key, err)
   322  	}
   323  
   324  	d.updatedKeys[key] = true
   325  
   326  	return nil
   327  }
   328  
   329  // ForceNew force-flags ForceNew in the schema for a specific key, and
   330  // re-calculates its diff, effectively causing this attribute to force a new
   331  // resource.
   332  //
   333  // Keep in mind that forcing a new resource will force a second run of the
   334  // resource's CustomizeDiff function (with a new ResourceDiff) once the current
   335  // one has completed. This second run is performed without state. This behavior
   336  // will be the same as if a new resource is being created and is performed to
   337  // ensure that the diff looks like the diff for a new resource as much as
   338  // possible. CustomizeDiff should expect such a scenario and act correctly.
   339  //
   340  // This function is a no-op/error if there is no diff.
   341  //
   342  // Note that the change to schema is permanent for the lifecycle of this
   343  // specific ResourceDiff instance.
   344  func (d *ResourceDiff) ForceNew(key string) error {
   345  	if !d.HasChange(key) {
   346  		return fmt.Errorf("ForceNew: No changes for %s", key)
   347  	}
   348  
   349  	keyParts := strings.Split(key, ".")
   350  	var schema *Schema
   351  	schemaL := addrToSchema(keyParts, d.schema)
   352  	if len(schemaL) > 0 {
   353  		schema = schemaL[len(schemaL)-1]
   354  	} else {
   355  		return fmt.Errorf("ForceNew: %s is not a valid key", key)
   356  	}
   357  
   358  	schema.ForceNew = true
   359  
   360  	// Flag this for a re-diff. Don't save any values to guarantee that existing
   361  	// diffs aren't messed with, as this gets messy when dealing with complex
   362  	// structures, zero values, etc.
   363  	d.forcedNewKeys[keyParts[0]] = true
   364  
   365  	return nil
   366  }
   367  
   368  // Get hands off to ResourceData.Get.
   369  func (d *ResourceDiff) Get(key string) interface{} {
   370  	r, _ := d.GetOk(key)
   371  	return r
   372  }
   373  
   374  // GetChange gets the change between the state and diff, checking first to see
   375  // if an overridden diff exists.
   376  //
   377  // This implementation differs from ResourceData's in the way that we first get
   378  // results from the exact levels for the new diff, then from state and diff as
   379  // per normal.
   380  func (d *ResourceDiff) GetChange(key string) (interface{}, interface{}) {
   381  	old, new, _ := d.getChange(key)
   382  	return old.Value, new.Value
   383  }
   384  
   385  // GetOk functions the same way as ResourceData.GetOk, but it also checks the
   386  // new diff levels to provide data consistent with the current state of the
   387  // customized diff.
   388  func (d *ResourceDiff) GetOk(key string) (interface{}, bool) {
   389  	r := d.get(strings.Split(key, "."), "newDiff")
   390  	exists := r.Exists && !r.Computed
   391  	if exists {
   392  		// If it exists, we also want to verify it is not the zero-value.
   393  		value := r.Value
   394  		zero := r.Schema.Type.Zero()
   395  
   396  		if eq, ok := value.(Equal); ok {
   397  			exists = !eq.Equal(zero)
   398  		} else {
   399  			exists = !reflect.DeepEqual(value, zero)
   400  		}
   401  	}
   402  
   403  	return r.Value, exists
   404  }
   405  
   406  // GetOkExists functions the same way as GetOkExists within ResourceData, but
   407  // it also checks the new diff levels to provide data consistent with the
   408  // current state of the customized diff.
   409  //
   410  // This is nearly the same function as GetOk, yet it does not check
   411  // for the zero value of the attribute's type. This allows for attributes
   412  // without a default, to fully check for a literal assignment, regardless
   413  // of the zero-value for that type.
   414  func (d *ResourceDiff) GetOkExists(key string) (interface{}, bool) {
   415  	r := d.get(strings.Split(key, "."), "newDiff")
   416  	exists := r.Exists && !r.Computed
   417  	return r.Value, exists
   418  }
   419  
   420  // NewValueKnown returns true if the new value for the given key is available
   421  // as its final value at diff time. If the return value is false, this means
   422  // either the value is based of interpolation that was unavailable at diff
   423  // time, or that the value was explicitly marked as computed by SetNewComputed.
   424  func (d *ResourceDiff) NewValueKnown(key string) bool {
   425  	r := d.get(strings.Split(key, "."), "newDiff")
   426  	return !r.Computed
   427  }
   428  
   429  // HasChange checks to see if there is a change between state and the diff, or
   430  // in the overridden diff.
   431  func (d *ResourceDiff) HasChange(key string) bool {
   432  	old, new := d.GetChange(key)
   433  
   434  	// If the type implements the Equal interface, then call that
   435  	// instead of just doing a reflect.DeepEqual. An example where this is
   436  	// needed is *Set
   437  	if eq, ok := old.(Equal); ok {
   438  		return !eq.Equal(new)
   439  	}
   440  
   441  	return !reflect.DeepEqual(old, new)
   442  }
   443  
   444  // Id returns the ID of this resource.
   445  //
   446  // Note that technically, ID does not change during diffs (it either has
   447  // already changed in the refresh, or will change on update), hence we do not
   448  // support updating the ID or fetching it from anything else other than state.
   449  func (d *ResourceDiff) Id() string {
   450  	var result string
   451  
   452  	if d.state != nil {
   453  		result = d.state.ID
   454  	}
   455  	return result
   456  }
   457  
   458  // getChange gets values from two different levels, designed for use in
   459  // diffChange, HasChange, and GetChange.
   460  //
   461  // This implementation differs from ResourceData's in the way that we first get
   462  // results from the exact levels for the new diff, then from state and diff as
   463  // per normal.
   464  func (d *ResourceDiff) getChange(key string) (getResult, getResult, bool) {
   465  	old := d.get(strings.Split(key, "."), "state")
   466  	var new getResult
   467  	for p := range d.updatedKeys {
   468  		if childAddrOf(key, p) {
   469  			new = d.getExact(strings.Split(key, "."), "newDiff")
   470  			return old, new, true
   471  		}
   472  	}
   473  	new = d.get(strings.Split(key, "."), "newDiff")
   474  	return old, new, false
   475  }
   476  
   477  // removed checks to see if the key is present in the existing, pre-customized
   478  // diff and if it was marked as NewRemoved.
   479  func (d *ResourceDiff) removed(k string) bool {
   480  	diff, ok := d.diff.Attributes[k]
   481  	if !ok {
   482  		return false
   483  	}
   484  	return diff.NewRemoved
   485  }
   486  
   487  // get performs the appropriate multi-level reader logic for ResourceDiff,
   488  // starting at source. Refer to newResourceDiff for the level order.
   489  func (d *ResourceDiff) get(addr []string, source string) getResult {
   490  	result, err := d.multiReader.ReadFieldMerge(addr, source)
   491  	if err != nil {
   492  		panic(err)
   493  	}
   494  
   495  	return d.finalizeResult(addr, result)
   496  }
   497  
   498  // getExact gets an attribute from the exact level referenced by source.
   499  func (d *ResourceDiff) getExact(addr []string, source string) getResult {
   500  	result, err := d.multiReader.ReadFieldExact(addr, source)
   501  	if err != nil {
   502  		panic(err)
   503  	}
   504  
   505  	return d.finalizeResult(addr, result)
   506  }
   507  
   508  // finalizeResult does some post-processing of the result produced by get and getExact.
   509  func (d *ResourceDiff) finalizeResult(addr []string, result FieldReadResult) getResult {
   510  	// If the result doesn't exist, then we set the value to the zero value
   511  	var schema *Schema
   512  	if schemaL := addrToSchema(addr, d.schema); len(schemaL) > 0 {
   513  		schema = schemaL[len(schemaL)-1]
   514  	}
   515  
   516  	if result.Value == nil && schema != nil {
   517  		result.Value = result.ValueOrZero(schema)
   518  	}
   519  
   520  	// Transform the FieldReadResult into a getResult. It might be worth
   521  	// merging these two structures one day.
   522  	return getResult{
   523  		Value:          result.Value,
   524  		ValueProcessed: result.ValueProcessed,
   525  		Computed:       result.Computed,
   526  		Exists:         result.Exists,
   527  		Schema:         schema,
   528  	}
   529  }
   530  
   531  // childAddrOf does a comparison of two addresses to see if one is the child of
   532  // the other.
   533  func childAddrOf(child, parent string) bool {
   534  	cs := strings.Split(child, ".")
   535  	ps := strings.Split(parent, ".")
   536  	if len(ps) > len(cs) {
   537  		return false
   538  	}
   539  	return reflect.DeepEqual(ps, cs[:len(ps)])
   540  }
   541  
   542  // checkKey checks the key to make sure it exists and is computed.
   543  func (d *ResourceDiff) checkKey(key, caller string, nested bool) error {
   544  	var schema *Schema
   545  	if nested {
   546  		keyParts := strings.Split(key, ".")
   547  		schemaL := addrToSchema(keyParts, d.schema)
   548  		if len(schemaL) > 0 {
   549  			schema = schemaL[len(schemaL)-1]
   550  		}
   551  	} else {
   552  		s, ok := d.schema[key]
   553  		if ok {
   554  			schema = s
   555  		}
   556  	}
   557  	if schema == nil {
   558  		return fmt.Errorf("%s: invalid key: %s", caller, key)
   559  	}
   560  	if !schema.Computed {
   561  		return fmt.Errorf("%s only operates on computed keys - %s is not one", caller, key)
   562  	}
   563  	return nil
   564  }