github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/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/iaas-resource-provision/iaas-rpc/internal/configs/configschema"
     9  )
    10  
    11  // ProposedNew 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 ProposedNew(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 = AllBlockAttributesNull(schema)
    41  	}
    42  	return proposedNew(schema, prior, config)
    43  }
    44  
    45  // PlannedDataResourceObject is similar to proposedNewBlock 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 proposedNewBlock 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 proposedNewBlock 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 proposedNew(schema, prior, config)
    65  }
    66  
    67  func proposedNew(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("ProposedNew 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  	newAttrs := proposedNewAttributes(schema.Attributes, prior, config)
    81  
    82  	// Merging nested blocks is a little more complex, since we need to
    83  	// correlate blocks between both objects and then recursively propose
    84  	// a new object for each. The correlation logic depends on the nesting
    85  	// mode for each block type.
    86  	for name, blockType := range schema.BlockTypes {
    87  		priorV := prior.GetAttr(name)
    88  		configV := config.GetAttr(name)
    89  		newAttrs[name] = proposedNewNestedBlock(blockType, priorV, configV)
    90  	}
    91  
    92  	return cty.ObjectVal(newAttrs)
    93  }
    94  
    95  func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty.Value) cty.Value {
    96  	// The only time we should encounter an entirely unknown block is from the
    97  	// use of dynamic with an unknown for_each expression.
    98  	if !config.IsKnown() {
    99  		return config
   100  	}
   101  
   102  	var newV cty.Value
   103  
   104  	switch schema.Nesting {
   105  
   106  	case configschema.NestingSingle, configschema.NestingGroup:
   107  		newV = ProposedNew(&schema.Block, prior, config)
   108  
   109  	case configschema.NestingList:
   110  		// Nested blocks are correlated by index.
   111  		configVLen := 0
   112  		if !config.IsNull() {
   113  			configVLen = config.LengthInt()
   114  		}
   115  		if configVLen > 0 {
   116  			newVals := make([]cty.Value, 0, configVLen)
   117  			for it := config.ElementIterator(); it.Next(); {
   118  				idx, configEV := it.Element()
   119  				if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) {
   120  					// If there is no corresponding prior element then
   121  					// we just take the config value as-is.
   122  					newVals = append(newVals, configEV)
   123  					continue
   124  				}
   125  				priorEV := prior.Index(idx)
   126  
   127  				newEV := ProposedNew(&schema.Block, priorEV, configEV)
   128  				newVals = append(newVals, newEV)
   129  			}
   130  			// Despite the name, a NestingList might also be a tuple, if
   131  			// its nested schema contains dynamically-typed attributes.
   132  			if config.Type().IsTupleType() {
   133  				newV = cty.TupleVal(newVals)
   134  			} else {
   135  				newV = cty.ListVal(newVals)
   136  			}
   137  		} else {
   138  			// Despite the name, a NestingList might also be a tuple, if
   139  			// its nested schema contains dynamically-typed attributes.
   140  			if config.Type().IsTupleType() {
   141  				newV = cty.EmptyTupleVal
   142  			} else {
   143  				newV = cty.ListValEmpty(schema.ImpliedType())
   144  			}
   145  		}
   146  
   147  	case configschema.NestingMap:
   148  		// Despite the name, a NestingMap may produce either a map or
   149  		// object value, depending on whether the nested schema contains
   150  		// dynamically-typed attributes.
   151  		if config.Type().IsObjectType() {
   152  			// Nested blocks are correlated by key.
   153  			configVLen := 0
   154  			if config.IsKnown() && !config.IsNull() {
   155  				configVLen = config.LengthInt()
   156  			}
   157  			if configVLen > 0 {
   158  				newVals := make(map[string]cty.Value, configVLen)
   159  				atys := config.Type().AttributeTypes()
   160  				for name := range atys {
   161  					configEV := config.GetAttr(name)
   162  					if !prior.IsKnown() || prior.IsNull() || !prior.Type().HasAttribute(name) {
   163  						// If there is no corresponding prior element then
   164  						// we just take the config value as-is.
   165  						newVals[name] = configEV
   166  						continue
   167  					}
   168  					priorEV := prior.GetAttr(name)
   169  
   170  					newEV := ProposedNew(&schema.Block, priorEV, configEV)
   171  					newVals[name] = newEV
   172  				}
   173  				// Although we call the nesting mode "map", we actually use
   174  				// object values so that elements might have different types
   175  				// in case of dynamically-typed attributes.
   176  				newV = cty.ObjectVal(newVals)
   177  			} else {
   178  				newV = cty.EmptyObjectVal
   179  			}
   180  		} else {
   181  			configVLen := 0
   182  			if config.IsKnown() && !config.IsNull() {
   183  				configVLen = config.LengthInt()
   184  			}
   185  			if configVLen > 0 {
   186  				newVals := make(map[string]cty.Value, configVLen)
   187  				for it := config.ElementIterator(); it.Next(); {
   188  					idx, configEV := it.Element()
   189  					k := idx.AsString()
   190  					if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) {
   191  						// If there is no corresponding prior element then
   192  						// we just take the config value as-is.
   193  						newVals[k] = configEV
   194  						continue
   195  					}
   196  					priorEV := prior.Index(idx)
   197  
   198  					newEV := ProposedNew(&schema.Block, priorEV, configEV)
   199  					newVals[k] = newEV
   200  				}
   201  				newV = cty.MapVal(newVals)
   202  			} else {
   203  				newV = cty.MapValEmpty(schema.ImpliedType())
   204  			}
   205  		}
   206  
   207  	case configschema.NestingSet:
   208  		if !config.Type().IsSetType() {
   209  			panic("configschema.NestingSet value is not a set as expected")
   210  		}
   211  
   212  		// Nested blocks are correlated by comparing the element values
   213  		// after eliminating all of the computed attributes. In practice,
   214  		// this means that any config change produces an entirely new
   215  		// nested object, and we only propagate prior computed values
   216  		// if the non-computed attribute values are identical.
   217  		var cmpVals [][2]cty.Value
   218  		if prior.IsKnown() && !prior.IsNull() {
   219  			cmpVals = setElementCompareValues(&schema.Block, prior, false)
   220  		}
   221  		configVLen := 0
   222  		if config.IsKnown() && !config.IsNull() {
   223  			configVLen = config.LengthInt()
   224  		}
   225  		if configVLen > 0 {
   226  			used := make([]bool, len(cmpVals)) // track used elements in case multiple have the same compare value
   227  			newVals := make([]cty.Value, 0, configVLen)
   228  			for it := config.ElementIterator(); it.Next(); {
   229  				_, configEV := it.Element()
   230  				var priorEV cty.Value
   231  				for i, cmp := range cmpVals {
   232  					if used[i] {
   233  						continue
   234  					}
   235  					if cmp[1].RawEquals(configEV) {
   236  						priorEV = cmp[0]
   237  						used[i] = true // we can't use this value on a future iteration
   238  						break
   239  					}
   240  				}
   241  				if priorEV == cty.NilVal {
   242  					priorEV = cty.NullVal(schema.ImpliedType())
   243  				}
   244  
   245  				newEV := ProposedNew(&schema.Block, priorEV, configEV)
   246  				newVals = append(newVals, newEV)
   247  			}
   248  			newV = cty.SetVal(newVals)
   249  		} else {
   250  			newV = cty.SetValEmpty(schema.Block.ImpliedType())
   251  		}
   252  
   253  	default:
   254  		// Should never happen, since the above cases are comprehensive.
   255  		panic(fmt.Sprintf("unsupported block nesting mode %s", schema.Nesting))
   256  	}
   257  	return newV
   258  }
   259  
   260  func proposedNewAttributes(attrs map[string]*configschema.Attribute, prior, config cty.Value) map[string]cty.Value {
   261  	if prior.IsNull() {
   262  		prior = AllAttributesNull(attrs)
   263  	}
   264  	newAttrs := make(map[string]cty.Value, len(attrs))
   265  	for name, attr := range attrs {
   266  		priorV := prior.GetAttr(name)
   267  		configV := config.GetAttr(name)
   268  		var newV cty.Value
   269  		switch {
   270  		case attr.Computed && attr.Optional:
   271  			// This is the trickiest scenario: we want to keep the prior value
   272  			// if the config isn't overriding it. Note that due to some
   273  			// ambiguity here, setting an optional+computed attribute from
   274  			// config and then later switching the config to null in a
   275  			// subsequent change causes the initial config value to be "sticky"
   276  			// unless the provider specifically overrides it during its own
   277  			// plan customization step.
   278  			if configV.IsNull() {
   279  				newV = priorV
   280  			} else {
   281  				newV = configV
   282  			}
   283  		case attr.Computed:
   284  			// configV will always be null in this case, by definition.
   285  			// priorV may also be null, but that's okay.
   286  			newV = priorV
   287  		default:
   288  			if attr.NestedType != nil {
   289  				// For non-computed NestedType attributes, we need to descend
   290  				// into the individual nested attributes to build the final
   291  				// value, unless the entire nested attribute is unknown.
   292  				if !configV.IsKnown() {
   293  					newV = configV
   294  				} else {
   295  					newV = proposedNewNestedType(attr.NestedType, priorV, configV)
   296  				}
   297  			} else {
   298  				// For non-computed attributes, we always take the config value,
   299  				// even if it is null. If it's _required_ then null values
   300  				// should've been caught during an earlier validation step, and
   301  				// so we don't really care about that here.
   302  				newV = configV
   303  			}
   304  		}
   305  		newAttrs[name] = newV
   306  	}
   307  	return newAttrs
   308  }
   309  
   310  func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value) cty.Value {
   311  	var newV cty.Value
   312  	switch schema.Nesting {
   313  	case configschema.NestingSingle:
   314  		if !config.IsNull() {
   315  			newV = cty.ObjectVal(proposedNewAttributes(schema.Attributes, prior, config))
   316  		} else {
   317  			newV = cty.NullVal(config.Type())
   318  		}
   319  
   320  	case configschema.NestingList:
   321  		// Nested blocks are correlated by index.
   322  		configVLen := 0
   323  		if config.IsKnown() && !config.IsNull() {
   324  			configVLen = config.LengthInt()
   325  		}
   326  		if configVLen > 0 {
   327  			newVals := make([]cty.Value, 0, configVLen)
   328  			for it := config.ElementIterator(); it.Next(); {
   329  				idx, configEV := it.Element()
   330  				if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) {
   331  					// If there is no corresponding prior element then
   332  					// we just take the config value as-is.
   333  					newVals = append(newVals, configEV)
   334  					continue
   335  				}
   336  				priorEV := prior.Index(idx)
   337  
   338  				newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV)
   339  				newVals = append(newVals, cty.ObjectVal(newEV))
   340  			}
   341  			// Despite the name, a NestingList might also be a tuple, if
   342  			// its nested schema contains dynamically-typed attributes.
   343  			if config.Type().IsTupleType() {
   344  				newV = cty.TupleVal(newVals)
   345  			} else {
   346  				newV = cty.ListVal(newVals)
   347  			}
   348  		} else {
   349  			newV = cty.NullVal(schema.ImpliedType())
   350  		}
   351  
   352  	case configschema.NestingMap:
   353  		// Despite the name, a NestingMap may produce either a map or
   354  		// object value, depending on whether the nested schema contains
   355  		// dynamically-typed attributes.
   356  		if config.Type().IsObjectType() {
   357  			// Nested blocks are correlated by key.
   358  			configVLen := 0
   359  			if config.IsKnown() && !config.IsNull() {
   360  				configVLen = config.LengthInt()
   361  			}
   362  			if configVLen > 0 {
   363  				newVals := make(map[string]cty.Value, configVLen)
   364  				atys := config.Type().AttributeTypes()
   365  				for name := range atys {
   366  					configEV := config.GetAttr(name)
   367  					if !prior.IsKnown() || prior.IsNull() || !prior.Type().HasAttribute(name) {
   368  						// If there is no corresponding prior element then
   369  						// we just take the config value as-is.
   370  						newVals[name] = configEV
   371  						continue
   372  					}
   373  					priorEV := prior.GetAttr(name)
   374  					newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV)
   375  					newVals[name] = cty.ObjectVal(newEV)
   376  				}
   377  				// Although we call the nesting mode "map", we actually use
   378  				// object values so that elements might have different types
   379  				// in case of dynamically-typed attributes.
   380  				newV = cty.ObjectVal(newVals)
   381  			} else {
   382  				newV = cty.NullVal(schema.ImpliedType())
   383  			}
   384  		} else {
   385  			configVLen := 0
   386  			if config.IsKnown() && !config.IsNull() {
   387  				configVLen = config.LengthInt()
   388  			}
   389  			if configVLen > 0 {
   390  				newVals := make(map[string]cty.Value, configVLen)
   391  				for it := config.ElementIterator(); it.Next(); {
   392  					idx, configEV := it.Element()
   393  					k := idx.AsString()
   394  					if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) {
   395  						// If there is no corresponding prior element then
   396  						// we just take the config value as-is.
   397  						newVals[k] = configEV
   398  						continue
   399  					}
   400  					priorEV := prior.Index(idx)
   401  
   402  					newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV)
   403  					newVals[k] = cty.ObjectVal(newEV)
   404  				}
   405  				newV = cty.MapVal(newVals)
   406  			} else {
   407  				newV = cty.NullVal(schema.ImpliedType())
   408  			}
   409  		}
   410  
   411  	case configschema.NestingSet:
   412  		// Nested blocks are correlated by comparing the element values
   413  		// after eliminating all of the computed attributes. In practice,
   414  		// this means that any config change produces an entirely new
   415  		// nested object, and we only propagate prior computed values
   416  		// if the non-computed attribute values are identical.
   417  		var cmpVals [][2]cty.Value
   418  		if prior.IsKnown() && !prior.IsNull() {
   419  			cmpVals = setElementCompareValuesFromObject(schema, prior)
   420  		}
   421  		configVLen := 0
   422  		if config.IsKnown() && !config.IsNull() {
   423  			configVLen = config.LengthInt()
   424  		}
   425  		if configVLen > 0 {
   426  			used := make([]bool, len(cmpVals)) // track used elements in case multiple have the same compare value
   427  			newVals := make([]cty.Value, 0, configVLen)
   428  			for it := config.ElementIterator(); it.Next(); {
   429  				_, configEV := it.Element()
   430  				var priorEV cty.Value
   431  				for i, cmp := range cmpVals {
   432  					if used[i] {
   433  						continue
   434  					}
   435  					if cmp[1].RawEquals(configEV) {
   436  						priorEV = cmp[0]
   437  						used[i] = true // we can't use this value on a future iteration
   438  						break
   439  					}
   440  				}
   441  				if priorEV == cty.NilVal {
   442  					newVals = append(newVals, configEV)
   443  				} else {
   444  					newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV)
   445  					newVals = append(newVals, cty.ObjectVal(newEV))
   446  				}
   447  			}
   448  			newV = cty.SetVal(newVals)
   449  		} else {
   450  			newV = cty.NullVal(schema.ImpliedType())
   451  		}
   452  	}
   453  
   454  	return newV
   455  }
   456  
   457  // setElementCompareValues takes a known, non-null value of a cty.Set type and
   458  // returns a table -- constructed of two-element arrays -- that maps original
   459  // set element values to corresponding values that have all of the computed
   460  // values removed, making them suitable for comparison with values obtained
   461  // from configuration. The element type of the set must conform to the implied
   462  // type of the given schema, or this function will panic.
   463  //
   464  // In the resulting slice, the zeroth element of each array is the original
   465  // value and the one-indexed element is the corresponding "compare value".
   466  //
   467  // This is intended to help correlate prior elements with configured elements
   468  // in proposedNewBlock. The result is a heuristic rather than an exact science,
   469  // since e.g. two separate elements may reduce to the same value through this
   470  // process. The caller must therefore be ready to deal with duplicates.
   471  func setElementCompareValues(schema *configschema.Block, set cty.Value, isConfig bool) [][2]cty.Value {
   472  	ret := make([][2]cty.Value, 0, set.LengthInt())
   473  	for it := set.ElementIterator(); it.Next(); {
   474  		_, ev := it.Element()
   475  		ret = append(ret, [2]cty.Value{ev, setElementCompareValue(schema, ev, isConfig)})
   476  	}
   477  	return ret
   478  }
   479  
   480  // setElementCompareValue creates a new value that has all of the same
   481  // non-computed attribute values as the one given but has all computed
   482  // attribute values forced to null.
   483  //
   484  // If isConfig is true then non-null Optional+Computed attribute values will
   485  // be preserved. Otherwise, they will also be set to null.
   486  //
   487  // The input value must conform to the schema's implied type, and the return
   488  // value is guaranteed to conform to it.
   489  func setElementCompareValue(schema *configschema.Block, v cty.Value, isConfig bool) cty.Value {
   490  	if v.IsNull() || !v.IsKnown() {
   491  		return v
   492  	}
   493  
   494  	attrs := map[string]cty.Value{}
   495  	for name, attr := range schema.Attributes {
   496  		switch {
   497  		case attr.Computed && attr.Optional:
   498  			if isConfig {
   499  				attrs[name] = v.GetAttr(name)
   500  			} else {
   501  				attrs[name] = cty.NullVal(attr.Type)
   502  			}
   503  		case attr.Computed:
   504  			attrs[name] = cty.NullVal(attr.Type)
   505  		default:
   506  			attrs[name] = v.GetAttr(name)
   507  		}
   508  	}
   509  
   510  	for name, blockType := range schema.BlockTypes {
   511  		elementType := blockType.Block.ImpliedType()
   512  
   513  		switch blockType.Nesting {
   514  		case configschema.NestingSingle, configschema.NestingGroup:
   515  			attrs[name] = setElementCompareValue(&blockType.Block, v.GetAttr(name), isConfig)
   516  
   517  		case configschema.NestingList, configschema.NestingSet:
   518  			cv := v.GetAttr(name)
   519  			if cv.IsNull() || !cv.IsKnown() {
   520  				attrs[name] = cv
   521  				continue
   522  			}
   523  
   524  			if l := cv.LengthInt(); l > 0 {
   525  				elems := make([]cty.Value, 0, l)
   526  				for it := cv.ElementIterator(); it.Next(); {
   527  					_, ev := it.Element()
   528  					elems = append(elems, setElementCompareValue(&blockType.Block, ev, isConfig))
   529  				}
   530  
   531  				switch {
   532  				case blockType.Nesting == configschema.NestingSet:
   533  					// SetValEmpty would panic if given elements that are not
   534  					// all of the same type, but that's guaranteed not to
   535  					// happen here because our input value was _already_ a
   536  					// set and we've not changed the types of any elements here.
   537  					attrs[name] = cty.SetVal(elems)
   538  
   539  				// NestingList cases
   540  				case elementType.HasDynamicTypes():
   541  					attrs[name] = cty.TupleVal(elems)
   542  				default:
   543  					attrs[name] = cty.ListVal(elems)
   544  				}
   545  			} else {
   546  				switch {
   547  				case blockType.Nesting == configschema.NestingSet:
   548  					attrs[name] = cty.SetValEmpty(elementType)
   549  
   550  				// NestingList cases
   551  				case elementType.HasDynamicTypes():
   552  					attrs[name] = cty.EmptyTupleVal
   553  				default:
   554  					attrs[name] = cty.ListValEmpty(elementType)
   555  				}
   556  			}
   557  
   558  		case configschema.NestingMap:
   559  			cv := v.GetAttr(name)
   560  			if cv.IsNull() || !cv.IsKnown() || cv.LengthInt() == 0 {
   561  				attrs[name] = cv
   562  				continue
   563  			}
   564  			elems := make(map[string]cty.Value)
   565  			for it := cv.ElementIterator(); it.Next(); {
   566  				kv, ev := it.Element()
   567  				elems[kv.AsString()] = setElementCompareValue(&blockType.Block, ev, isConfig)
   568  			}
   569  
   570  			switch {
   571  			case elementType.HasDynamicTypes():
   572  				attrs[name] = cty.ObjectVal(elems)
   573  			default:
   574  				attrs[name] = cty.MapVal(elems)
   575  			}
   576  
   577  		default:
   578  			// Should never happen, since the above cases are comprehensive.
   579  			panic(fmt.Sprintf("unsupported block nesting mode %s", blockType.Nesting))
   580  		}
   581  	}
   582  
   583  	return cty.ObjectVal(attrs)
   584  }
   585  
   586  // setElementCompareValues takes a known, non-null value of a cty.Set type and
   587  // returns a table -- constructed of two-element arrays -- that maps original
   588  // set element values to corresponding values that have all of the computed
   589  // values removed, making them suitable for comparison with values obtained
   590  // from configuration. The element type of the set must conform to the implied
   591  // type of the given schema, or this function will panic.
   592  //
   593  // In the resulting slice, the zeroth element of each array is the original
   594  // value and the one-indexed element is the corresponding "compare value".
   595  //
   596  // This is intended to help correlate prior elements with configured elements
   597  // in proposedNewBlock. The result is a heuristic rather than an exact science,
   598  // since e.g. two separate elements may reduce to the same value through this
   599  // process. The caller must therefore be ready to deal with duplicates.
   600  func setElementCompareValuesFromObject(schema *configschema.Object, set cty.Value) [][2]cty.Value {
   601  	ret := make([][2]cty.Value, 0, set.LengthInt())
   602  	for it := set.ElementIterator(); it.Next(); {
   603  		_, ev := it.Element()
   604  		ret = append(ret, [2]cty.Value{ev, setElementCompareValueFromObject(schema, ev)})
   605  	}
   606  	return ret
   607  }
   608  
   609  // setElementCompareValue creates a new value that has all of the same
   610  // non-computed attribute values as the one given but has all computed
   611  // attribute values forced to null.
   612  //
   613  // The input value must conform to the schema's implied type, and the return
   614  // value is guaranteed to conform to it.
   615  func setElementCompareValueFromObject(schema *configschema.Object, v cty.Value) cty.Value {
   616  	if v.IsNull() || !v.IsKnown() {
   617  		return v
   618  	}
   619  	attrs := map[string]cty.Value{}
   620  
   621  	for name, attr := range schema.Attributes {
   622  		attrV := v.GetAttr(name)
   623  		switch {
   624  		case attr.Computed:
   625  			attrs[name] = cty.NullVal(attr.Type)
   626  		default:
   627  			attrs[name] = attrV
   628  		}
   629  	}
   630  
   631  	return cty.ObjectVal(attrs)
   632  }