kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/plans/objchange/objchange.go (about)

     1  package objchange
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/zclconf/go-cty/cty"
     7  
     8  	"kubeform.dev/terraform-backend-sdk/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  	// If the config is null or empty, we will be using this default value.
   312  	newV := config
   313  
   314  	switch schema.Nesting {
   315  	case configschema.NestingSingle:
   316  		if !config.IsNull() {
   317  			newV = cty.ObjectVal(proposedNewAttributes(schema.Attributes, prior, config))
   318  		} else {
   319  			newV = cty.NullVal(config.Type())
   320  		}
   321  
   322  	case configschema.NestingList:
   323  		// Nested blocks are correlated by index.
   324  		configVLen := 0
   325  		if config.IsKnown() && !config.IsNull() {
   326  			configVLen = config.LengthInt()
   327  		}
   328  
   329  		if configVLen > 0 {
   330  			newVals := make([]cty.Value, 0, configVLen)
   331  			for it := config.ElementIterator(); it.Next(); {
   332  				idx, configEV := it.Element()
   333  				if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) {
   334  					// If there is no corresponding prior element then
   335  					// we just take the config value as-is.
   336  					newVals = append(newVals, configEV)
   337  					continue
   338  				}
   339  				priorEV := prior.Index(idx)
   340  
   341  				newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV)
   342  				newVals = append(newVals, cty.ObjectVal(newEV))
   343  			}
   344  			// Despite the name, a NestingList might also be a tuple, if
   345  			// its nested schema contains dynamically-typed attributes.
   346  			if config.Type().IsTupleType() {
   347  				newV = cty.TupleVal(newVals)
   348  			} else {
   349  				newV = cty.ListVal(newVals)
   350  			}
   351  		}
   352  
   353  	case configschema.NestingMap:
   354  		// Despite the name, a NestingMap may produce either a map or
   355  		// object value, depending on whether the nested schema contains
   356  		// dynamically-typed attributes.
   357  		if config.Type().IsObjectType() {
   358  			// Nested blocks are correlated by key.
   359  			configVLen := 0
   360  			if config.IsKnown() && !config.IsNull() {
   361  				configVLen = config.LengthInt()
   362  			}
   363  			if configVLen > 0 {
   364  				newVals := make(map[string]cty.Value, configVLen)
   365  				atys := config.Type().AttributeTypes()
   366  				for name := range atys {
   367  					configEV := config.GetAttr(name)
   368  					if !prior.IsKnown() || prior.IsNull() || !prior.Type().HasAttribute(name) {
   369  						// If there is no corresponding prior element then
   370  						// we just take the config value as-is.
   371  						newVals[name] = configEV
   372  						continue
   373  					}
   374  					priorEV := prior.GetAttr(name)
   375  					newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV)
   376  					newVals[name] = cty.ObjectVal(newEV)
   377  				}
   378  				// Although we call the nesting mode "map", we actually use
   379  				// object values so that elements might have different types
   380  				// in case of dynamically-typed attributes.
   381  				newV = cty.ObjectVal(newVals)
   382  			}
   383  		} else {
   384  			configVLen := 0
   385  			if config.IsKnown() && !config.IsNull() {
   386  				configVLen = config.LengthInt()
   387  			}
   388  			if configVLen > 0 {
   389  				newVals := make(map[string]cty.Value, configVLen)
   390  				for it := config.ElementIterator(); it.Next(); {
   391  					idx, configEV := it.Element()
   392  					k := idx.AsString()
   393  					if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) {
   394  						// If there is no corresponding prior element then
   395  						// we just take the config value as-is.
   396  						newVals[k] = configEV
   397  						continue
   398  					}
   399  					priorEV := prior.Index(idx)
   400  
   401  					newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV)
   402  					newVals[k] = cty.ObjectVal(newEV)
   403  				}
   404  				newV = cty.MapVal(newVals)
   405  			}
   406  		}
   407  
   408  	case configschema.NestingSet:
   409  		// Nested blocks are correlated by comparing the element values
   410  		// after eliminating all of the computed attributes. In practice,
   411  		// this means that any config change produces an entirely new
   412  		// nested object, and we only propagate prior computed values
   413  		// if the non-computed attribute values are identical.
   414  		var cmpVals [][2]cty.Value
   415  		if prior.IsKnown() && !prior.IsNull() {
   416  			cmpVals = setElementCompareValuesFromObject(schema, prior)
   417  		}
   418  		configVLen := 0
   419  		if config.IsKnown() && !config.IsNull() {
   420  			configVLen = config.LengthInt()
   421  		}
   422  		if configVLen > 0 {
   423  			used := make([]bool, len(cmpVals)) // track used elements in case multiple have the same compare value
   424  			newVals := make([]cty.Value, 0, configVLen)
   425  			for it := config.ElementIterator(); it.Next(); {
   426  				_, configEV := it.Element()
   427  				var priorEV cty.Value
   428  				for i, cmp := range cmpVals {
   429  					if used[i] {
   430  						continue
   431  					}
   432  					if cmp[1].RawEquals(configEV) {
   433  						priorEV = cmp[0]
   434  						used[i] = true // we can't use this value on a future iteration
   435  						break
   436  					}
   437  				}
   438  				if priorEV == cty.NilVal {
   439  					newVals = append(newVals, configEV)
   440  				} else {
   441  					newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV)
   442  					newVals = append(newVals, cty.ObjectVal(newEV))
   443  				}
   444  			}
   445  			newV = cty.SetVal(newVals)
   446  		}
   447  	}
   448  
   449  	return newV
   450  }
   451  
   452  // setElementCompareValues takes a known, non-null value of a cty.Set type and
   453  // returns a table -- constructed of two-element arrays -- that maps original
   454  // set element values to corresponding values that have all of the computed
   455  // values removed, making them suitable for comparison with values obtained
   456  // from configuration. The element type of the set must conform to the implied
   457  // type of the given schema, or this function will panic.
   458  //
   459  // In the resulting slice, the zeroth element of each array is the original
   460  // value and the one-indexed element is the corresponding "compare value".
   461  //
   462  // This is intended to help correlate prior elements with configured elements
   463  // in proposedNewBlock. The result is a heuristic rather than an exact science,
   464  // since e.g. two separate elements may reduce to the same value through this
   465  // process. The caller must therefore be ready to deal with duplicates.
   466  func setElementCompareValues(schema *configschema.Block, set cty.Value, isConfig bool) [][2]cty.Value {
   467  	ret := make([][2]cty.Value, 0, set.LengthInt())
   468  	for it := set.ElementIterator(); it.Next(); {
   469  		_, ev := it.Element()
   470  		ret = append(ret, [2]cty.Value{ev, setElementCompareValue(schema, ev, isConfig)})
   471  	}
   472  	return ret
   473  }
   474  
   475  // setElementCompareValue creates a new value that has all of the same
   476  // non-computed attribute values as the one given but has all computed
   477  // attribute values forced to null.
   478  //
   479  // If isConfig is true then non-null Optional+Computed attribute values will
   480  // be preserved. Otherwise, they will also be set to null.
   481  //
   482  // The input value must conform to the schema's implied type, and the return
   483  // value is guaranteed to conform to it.
   484  func setElementCompareValue(schema *configschema.Block, v cty.Value, isConfig bool) cty.Value {
   485  	if v.IsNull() || !v.IsKnown() {
   486  		return v
   487  	}
   488  
   489  	attrs := map[string]cty.Value{}
   490  	for name, attr := range schema.Attributes {
   491  		switch {
   492  		case attr.Computed && attr.Optional:
   493  			if isConfig {
   494  				attrs[name] = v.GetAttr(name)
   495  			} else {
   496  				attrs[name] = cty.NullVal(attr.Type)
   497  			}
   498  		case attr.Computed:
   499  			attrs[name] = cty.NullVal(attr.Type)
   500  		default:
   501  			attrs[name] = v.GetAttr(name)
   502  		}
   503  	}
   504  
   505  	for name, blockType := range schema.BlockTypes {
   506  		elementType := blockType.Block.ImpliedType()
   507  
   508  		switch blockType.Nesting {
   509  		case configschema.NestingSingle, configschema.NestingGroup:
   510  			attrs[name] = setElementCompareValue(&blockType.Block, v.GetAttr(name), isConfig)
   511  
   512  		case configschema.NestingList, configschema.NestingSet:
   513  			cv := v.GetAttr(name)
   514  			if cv.IsNull() || !cv.IsKnown() {
   515  				attrs[name] = cv
   516  				continue
   517  			}
   518  
   519  			if l := cv.LengthInt(); l > 0 {
   520  				elems := make([]cty.Value, 0, l)
   521  				for it := cv.ElementIterator(); it.Next(); {
   522  					_, ev := it.Element()
   523  					elems = append(elems, setElementCompareValue(&blockType.Block, ev, isConfig))
   524  				}
   525  
   526  				switch {
   527  				case blockType.Nesting == configschema.NestingSet:
   528  					// SetValEmpty would panic if given elements that are not
   529  					// all of the same type, but that's guaranteed not to
   530  					// happen here because our input value was _already_ a
   531  					// set and we've not changed the types of any elements here.
   532  					attrs[name] = cty.SetVal(elems)
   533  
   534  				// NestingList cases
   535  				case elementType.HasDynamicTypes():
   536  					attrs[name] = cty.TupleVal(elems)
   537  				default:
   538  					attrs[name] = cty.ListVal(elems)
   539  				}
   540  			} else {
   541  				switch {
   542  				case blockType.Nesting == configschema.NestingSet:
   543  					attrs[name] = cty.SetValEmpty(elementType)
   544  
   545  				// NestingList cases
   546  				case elementType.HasDynamicTypes():
   547  					attrs[name] = cty.EmptyTupleVal
   548  				default:
   549  					attrs[name] = cty.ListValEmpty(elementType)
   550  				}
   551  			}
   552  
   553  		case configschema.NestingMap:
   554  			cv := v.GetAttr(name)
   555  			if cv.IsNull() || !cv.IsKnown() || cv.LengthInt() == 0 {
   556  				attrs[name] = cv
   557  				continue
   558  			}
   559  			elems := make(map[string]cty.Value)
   560  			for it := cv.ElementIterator(); it.Next(); {
   561  				kv, ev := it.Element()
   562  				elems[kv.AsString()] = setElementCompareValue(&blockType.Block, ev, isConfig)
   563  			}
   564  
   565  			switch {
   566  			case elementType.HasDynamicTypes():
   567  				attrs[name] = cty.ObjectVal(elems)
   568  			default:
   569  				attrs[name] = cty.MapVal(elems)
   570  			}
   571  
   572  		default:
   573  			// Should never happen, since the above cases are comprehensive.
   574  			panic(fmt.Sprintf("unsupported block nesting mode %s", blockType.Nesting))
   575  		}
   576  	}
   577  
   578  	return cty.ObjectVal(attrs)
   579  }
   580  
   581  // setElementCompareValues takes a known, non-null value of a cty.Set type and
   582  // returns a table -- constructed of two-element arrays -- that maps original
   583  // set element values to corresponding values that have all of the computed
   584  // values removed, making them suitable for comparison with values obtained
   585  // from configuration. The element type of the set must conform to the implied
   586  // type of the given schema, or this function will panic.
   587  //
   588  // In the resulting slice, the zeroth element of each array is the original
   589  // value and the one-indexed element is the corresponding "compare value".
   590  //
   591  // This is intended to help correlate prior elements with configured elements
   592  // in proposedNewBlock. The result is a heuristic rather than an exact science,
   593  // since e.g. two separate elements may reduce to the same value through this
   594  // process. The caller must therefore be ready to deal with duplicates.
   595  func setElementCompareValuesFromObject(schema *configschema.Object, set cty.Value) [][2]cty.Value {
   596  	ret := make([][2]cty.Value, 0, set.LengthInt())
   597  	for it := set.ElementIterator(); it.Next(); {
   598  		_, ev := it.Element()
   599  		ret = append(ret, [2]cty.Value{ev, setElementCompareValueFromObject(schema, ev)})
   600  	}
   601  	return ret
   602  }
   603  
   604  // setElementCompareValue creates a new value that has all of the same
   605  // non-computed attribute values as the one given but has all computed
   606  // attribute values forced to null.
   607  //
   608  // The input value must conform to the schema's implied type, and the return
   609  // value is guaranteed to conform to it.
   610  func setElementCompareValueFromObject(schema *configschema.Object, v cty.Value) cty.Value {
   611  	if v.IsNull() || !v.IsKnown() {
   612  		return v
   613  	}
   614  	attrs := map[string]cty.Value{}
   615  
   616  	for name, attr := range schema.Attributes {
   617  		attrV := v.GetAttr(name)
   618  		switch {
   619  		case attr.Computed:
   620  			attrs[name] = cty.NullVal(attr.Type)
   621  		default:
   622  			attrs[name] = attrV
   623  		}
   624  	}
   625  
   626  	return cty.ObjectVal(attrs)
   627  }