github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/plans/objchange/objchange.go (about)

     1  package objchange
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/zclconf/go-cty/cty"
     7  
     8  	"github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema"
     9  )
    10  
    11  // ProposedNewObject constructs a proposed new object value by combining the
    12  // computed attribute values from "prior" with the configured attribute values
    13  // from "config".
    14  //
    15  // Both value must conform to the given schema's implied type, or this function
    16  // will panic.
    17  //
    18  // The prior value must be wholly known, but the config value may be unknown
    19  // or have nested unknown values.
    20  //
    21  // The merging of the two objects includes the attributes of any nested blocks,
    22  // which will be correlated in a manner appropriate for their nesting mode.
    23  // Note in particular that the correlation for blocks backed by sets is a
    24  // heuristic based on matching non-computed attribute values and so it may
    25  // produce strange results with more "extreme" cases, such as a nested set
    26  // block where _all_ attributes are computed.
    27  func ProposedNewObject(schema *configschema.Block, prior, config cty.Value) cty.Value {
    28  	// If the config and prior are both null, return early here before
    29  	// populating the prior block. The prevents non-null blocks from appearing
    30  	// the proposed state value.
    31  	if config.IsNull() && prior.IsNull() {
    32  		return prior
    33  	}
    34  
    35  	if prior.IsNull() {
    36  		// In this case, we will construct a synthetic prior value that is
    37  		// similar to the result of decoding an empty configuration block,
    38  		// which simplifies our handling of the top-level attributes/blocks
    39  		// below by giving us one non-null level of object to pull values from.
    40  		prior = AllAttributesNull(schema)
    41  	}
    42  	return proposedNewObject(schema, prior, config)
    43  }
    44  
    45  // PlannedDataResourceObject is similar to ProposedNewObject but tailored for
    46  // planning data resources in particular. Specifically, it replaces the values
    47  // of any Computed attributes not set in the configuration with an unknown
    48  // value, which serves as a placeholder for a value to be filled in by the
    49  // provider when the data resource is finally read.
    50  //
    51  // Data resources are different because the planning of them is handled
    52  // entirely within Terraform Core and not subject to customization by the
    53  // provider. This function is, in effect, producing an equivalent result to
    54  // passing the ProposedNewObject result into a provider's PlanResourceChange
    55  // function, assuming a fixed implementation of PlanResourceChange that just
    56  // fills in unknown values as needed.
    57  func PlannedDataResourceObject(schema *configschema.Block, config cty.Value) cty.Value {
    58  	// Our trick here is to run the ProposedNewObject logic with an
    59  	// entirely-unknown prior value. Because of cty's unknown short-circuit
    60  	// behavior, any operation on prior returns another unknown, and so
    61  	// unknown values propagate into all of the parts of the resulting value
    62  	// that would normally be filled in by preserving the prior state.
    63  	prior := cty.UnknownVal(schema.ImpliedType())
    64  	return proposedNewObject(schema, prior, config)
    65  }
    66  
    67  func proposedNewObject(schema *configschema.Block, prior, config cty.Value) cty.Value {
    68  	if config.IsNull() || !config.IsKnown() {
    69  		// This is a weird situation, but we'll allow it anyway to free
    70  		// callers from needing to specifically check for these cases.
    71  		return prior
    72  	}
    73  	if (!prior.Type().IsObjectType()) || (!config.Type().IsObjectType()) {
    74  		panic("ProposedNewObject only supports object-typed values")
    75  	}
    76  
    77  	// From this point onwards, we can assume that both values are non-null
    78  	// object types, and that the config value itself is known (though it
    79  	// may contain nested values that are unknown.)
    80  
    81  	newAttrs := map[string]cty.Value{}
    82  	for name, attr := range schema.Attributes {
    83  		priorV := prior.GetAttr(name)
    84  		configV := config.GetAttr(name)
    85  		var newV cty.Value
    86  		switch {
    87  		case attr.Computed && attr.Optional:
    88  			// This is the trickiest scenario: we want to keep the prior value
    89  			// if the config isn't overriding it. Note that due to some
    90  			// ambiguity here, setting an optional+computed attribute from
    91  			// config and then later switching the config to null in a
    92  			// subsequent change causes the initial config value to be "sticky"
    93  			// unless the provider specifically overrides it during its own
    94  			// plan customization step.
    95  			if configV.IsNull() {
    96  				newV = priorV
    97  			} else {
    98  				newV = configV
    99  			}
   100  		case attr.Computed:
   101  			// configV will always be null in this case, by definition.
   102  			// priorV may also be null, but that's okay.
   103  			newV = priorV
   104  		default:
   105  			// For non-computed attributes, we always take the config value,
   106  			// even if it is null. If it's _required_ then null values
   107  			// should've been caught during an earlier validation step, and
   108  			// so we don't really care about that here.
   109  			newV = configV
   110  		}
   111  		newAttrs[name] = newV
   112  	}
   113  
   114  	// Merging nested blocks is a little more complex, since we need to
   115  	// correlate blocks between both objects and then recursively propose
   116  	// a new object for each. The correlation logic depends on the nesting
   117  	// mode for each block type.
   118  	for name, blockType := range schema.BlockTypes {
   119  		priorV := prior.GetAttr(name)
   120  		configV := config.GetAttr(name)
   121  		var newV cty.Value
   122  		switch blockType.Nesting {
   123  
   124  		case configschema.NestingSingle, configschema.NestingGroup:
   125  			newV = ProposedNewObject(&blockType.Block, priorV, configV)
   126  
   127  		case configschema.NestingList:
   128  			// Nested blocks are correlated by index.
   129  			configVLen := 0
   130  			if configV.IsKnown() && !configV.IsNull() {
   131  				configVLen = configV.LengthInt()
   132  			}
   133  			if configVLen > 0 {
   134  				newVals := make([]cty.Value, 0, configVLen)
   135  				for it := configV.ElementIterator(); it.Next(); {
   136  					idx, configEV := it.Element()
   137  					if priorV.IsKnown() && (priorV.IsNull() || !priorV.HasIndex(idx).True()) {
   138  						// If there is no corresponding prior element then
   139  						// we just take the config value as-is.
   140  						newVals = append(newVals, configEV)
   141  						continue
   142  					}
   143  					priorEV := priorV.Index(idx)
   144  
   145  					newEV := ProposedNewObject(&blockType.Block, priorEV, configEV)
   146  					newVals = append(newVals, newEV)
   147  				}
   148  				// Despite the name, a NestingList might also be a tuple, if
   149  				// its nested schema contains dynamically-typed attributes.
   150  				if configV.Type().IsTupleType() {
   151  					newV = cty.TupleVal(newVals)
   152  				} else {
   153  					newV = cty.ListVal(newVals)
   154  				}
   155  			} else {
   156  				// Despite the name, a NestingList might also be a tuple, if
   157  				// its nested schema contains dynamically-typed attributes.
   158  				if configV.Type().IsTupleType() {
   159  					newV = cty.EmptyTupleVal
   160  				} else {
   161  					newV = cty.ListValEmpty(blockType.ImpliedType())
   162  				}
   163  			}
   164  
   165  		case configschema.NestingMap:
   166  			// Despite the name, a NestingMap may produce either a map or
   167  			// object value, depending on whether the nested schema contains
   168  			// dynamically-typed attributes.
   169  			if configV.Type().IsObjectType() {
   170  				// Nested blocks are correlated by key.
   171  				configVLen := 0
   172  				if configV.IsKnown() && !configV.IsNull() {
   173  					configVLen = configV.LengthInt()
   174  				}
   175  				if configVLen > 0 {
   176  					newVals := make(map[string]cty.Value, configVLen)
   177  					atys := configV.Type().AttributeTypes()
   178  					for name := range atys {
   179  						configEV := configV.GetAttr(name)
   180  						if !priorV.IsKnown() || priorV.IsNull() || !priorV.Type().HasAttribute(name) {
   181  							// If there is no corresponding prior element then
   182  							// we just take the config value as-is.
   183  							newVals[name] = configEV
   184  							continue
   185  						}
   186  						priorEV := priorV.GetAttr(name)
   187  
   188  						newEV := ProposedNewObject(&blockType.Block, priorEV, configEV)
   189  						newVals[name] = newEV
   190  					}
   191  					// Although we call the nesting mode "map", we actually use
   192  					// object values so that elements might have different types
   193  					// in case of dynamically-typed attributes.
   194  					newV = cty.ObjectVal(newVals)
   195  				} else {
   196  					newV = cty.EmptyObjectVal
   197  				}
   198  			} else {
   199  				configVLen := 0
   200  				if configV.IsKnown() && !configV.IsNull() {
   201  					configVLen = configV.LengthInt()
   202  				}
   203  				if configVLen > 0 {
   204  					newVals := make(map[string]cty.Value, configVLen)
   205  					for it := configV.ElementIterator(); it.Next(); {
   206  						idx, configEV := it.Element()
   207  						k := idx.AsString()
   208  						if priorV.IsKnown() && (priorV.IsNull() || !priorV.HasIndex(idx).True()) {
   209  							// If there is no corresponding prior element then
   210  							// we just take the config value as-is.
   211  							newVals[k] = configEV
   212  							continue
   213  						}
   214  						priorEV := priorV.Index(idx)
   215  
   216  						newEV := ProposedNewObject(&blockType.Block, priorEV, configEV)
   217  						newVals[k] = newEV
   218  					}
   219  					newV = cty.MapVal(newVals)
   220  				} else {
   221  					newV = cty.MapValEmpty(blockType.ImpliedType())
   222  				}
   223  			}
   224  
   225  		case configschema.NestingSet:
   226  			if !configV.Type().IsSetType() {
   227  				panic("configschema.NestingSet value is not a set as expected")
   228  			}
   229  
   230  			// Nested blocks are correlated by comparing the element values
   231  			// after eliminating all of the computed attributes. In practice,
   232  			// this means that any config change produces an entirely new
   233  			// nested object, and we only propagate prior computed values
   234  			// if the non-computed attribute values are identical.
   235  			var cmpVals [][2]cty.Value
   236  			if priorV.IsKnown() && !priorV.IsNull() {
   237  				cmpVals = setElementCompareValues(&blockType.Block, priorV, false)
   238  			}
   239  			configVLen := 0
   240  			if configV.IsKnown() && !configV.IsNull() {
   241  				configVLen = configV.LengthInt()
   242  			}
   243  			if configVLen > 0 {
   244  				used := make([]bool, len(cmpVals)) // track used elements in case multiple have the same compare value
   245  				newVals := make([]cty.Value, 0, configVLen)
   246  				for it := configV.ElementIterator(); it.Next(); {
   247  					_, configEV := it.Element()
   248  					var priorEV cty.Value
   249  					for i, cmp := range cmpVals {
   250  						if used[i] {
   251  							continue
   252  						}
   253  						if cmp[1].RawEquals(configEV) {
   254  							priorEV = cmp[0]
   255  							used[i] = true // we can't use this value on a future iteration
   256  							break
   257  						}
   258  					}
   259  					if priorEV == cty.NilVal {
   260  						priorEV = cty.NullVal(blockType.ImpliedType())
   261  					}
   262  
   263  					newEV := ProposedNewObject(&blockType.Block, priorEV, configEV)
   264  					newVals = append(newVals, newEV)
   265  				}
   266  				newV = cty.SetVal(newVals)
   267  			} else {
   268  				newV = cty.SetValEmpty(blockType.Block.ImpliedType())
   269  			}
   270  
   271  		default:
   272  			// Should never happen, since the above cases are comprehensive.
   273  			panic(fmt.Sprintf("unsupported block nesting mode %s", blockType.Nesting))
   274  		}
   275  
   276  		newAttrs[name] = newV
   277  	}
   278  
   279  	return cty.ObjectVal(newAttrs)
   280  }
   281  
   282  // setElementCompareValues takes a known, non-null value of a cty.Set type and
   283  // returns a table -- constructed of two-element arrays -- that maps original
   284  // set element values to corresponding values that have all of the computed
   285  // values removed, making them suitable for comparison with values obtained
   286  // from configuration. The element type of the set must conform to the implied
   287  // type of the given schema, or this function will panic.
   288  //
   289  // In the resulting slice, the zeroth element of each array is the original
   290  // value and the one-indexed element is the corresponding "compare value".
   291  //
   292  // This is intended to help correlate prior elements with configured elements
   293  // in ProposedNewObject. The result is a heuristic rather than an exact science,
   294  // since e.g. two separate elements may reduce to the same value through this
   295  // process. The caller must therefore be ready to deal with duplicates.
   296  func setElementCompareValues(schema *configschema.Block, set cty.Value, isConfig bool) [][2]cty.Value {
   297  	ret := make([][2]cty.Value, 0, set.LengthInt())
   298  	for it := set.ElementIterator(); it.Next(); {
   299  		_, ev := it.Element()
   300  		ret = append(ret, [2]cty.Value{ev, setElementCompareValue(schema, ev, isConfig)})
   301  	}
   302  	return ret
   303  }
   304  
   305  // setElementCompareValue creates a new value that has all of the same
   306  // non-computed attribute values as the one given but has all computed
   307  // attribute values forced to null.
   308  //
   309  // If isConfig is true then non-null Optional+Computed attribute values will
   310  // be preserved. Otherwise, they will also be set to null.
   311  //
   312  // The input value must conform to the schema's implied type, and the return
   313  // value is guaranteed to conform to it.
   314  func setElementCompareValue(schema *configschema.Block, v cty.Value, isConfig bool) cty.Value {
   315  	if v.IsNull() || !v.IsKnown() {
   316  		return v
   317  	}
   318  
   319  	attrs := map[string]cty.Value{}
   320  	for name, attr := range schema.Attributes {
   321  		switch {
   322  		case attr.Computed && attr.Optional:
   323  			if isConfig {
   324  				attrs[name] = v.GetAttr(name)
   325  			} else {
   326  				attrs[name] = cty.NullVal(attr.Type)
   327  			}
   328  		case attr.Computed:
   329  			attrs[name] = cty.NullVal(attr.Type)
   330  		default:
   331  			attrs[name] = v.GetAttr(name)
   332  		}
   333  	}
   334  
   335  	for name, blockType := range schema.BlockTypes {
   336  		switch blockType.Nesting {
   337  
   338  		case configschema.NestingSingle, configschema.NestingGroup:
   339  			attrs[name] = setElementCompareValue(&blockType.Block, v.GetAttr(name), isConfig)
   340  
   341  		case configschema.NestingList, configschema.NestingSet:
   342  			cv := v.GetAttr(name)
   343  			if cv.IsNull() || !cv.IsKnown() {
   344  				attrs[name] = cv
   345  				continue
   346  			}
   347  			if l := cv.LengthInt(); l > 0 {
   348  				elems := make([]cty.Value, 0, l)
   349  				for it := cv.ElementIterator(); it.Next(); {
   350  					_, ev := it.Element()
   351  					elems = append(elems, setElementCompareValue(&blockType.Block, ev, isConfig))
   352  				}
   353  				if blockType.Nesting == configschema.NestingSet {
   354  					// SetValEmpty would panic if given elements that are not
   355  					// all of the same type, but that's guaranteed not to
   356  					// happen here because our input value was _already_ a
   357  					// set and we've not changed the types of any elements here.
   358  					attrs[name] = cty.SetVal(elems)
   359  				} else {
   360  					attrs[name] = cty.TupleVal(elems)
   361  				}
   362  			} else {
   363  				if blockType.Nesting == configschema.NestingSet {
   364  					attrs[name] = cty.SetValEmpty(blockType.Block.ImpliedType())
   365  				} else {
   366  					attrs[name] = cty.EmptyTupleVal
   367  				}
   368  			}
   369  
   370  		case configschema.NestingMap:
   371  			cv := v.GetAttr(name)
   372  			if cv.IsNull() || !cv.IsKnown() {
   373  				attrs[name] = cv
   374  				continue
   375  			}
   376  			elems := make(map[string]cty.Value)
   377  			for it := cv.ElementIterator(); it.Next(); {
   378  				kv, ev := it.Element()
   379  				elems[kv.AsString()] = setElementCompareValue(&blockType.Block, ev, isConfig)
   380  			}
   381  			attrs[name] = cty.ObjectVal(elems)
   382  
   383  		default:
   384  			// Should never happen, since the above cases are comprehensive.
   385  			panic(fmt.Sprintf("unsupported block nesting mode %s", blockType.Nesting))
   386  		}
   387  	}
   388  
   389  	return cty.ObjectVal(attrs)
   390  }