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