github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/plans/objchange/objchange.go (about)

     1  package objchange
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  
     7  	"github.com/zclconf/go-cty/cty"
     8  
     9  	"github.com/hashicorp/terraform/internal/configs/configschema"
    10  )
    11  
    12  // ProposedNew constructs a proposed new object value by combining the
    13  // computed attribute values from "prior" with the configured attribute values
    14  // from "config".
    15  //
    16  // Both value must conform to the given schema's implied type, or this function
    17  // will panic.
    18  //
    19  // The prior value must be wholly known, but the config value may be unknown
    20  // or have nested unknown values.
    21  //
    22  // The merging of the two objects includes the attributes of any nested blocks,
    23  // which will be correlated in a manner appropriate for their nesting mode.
    24  // Note in particular that the correlation for blocks backed by sets is a
    25  // heuristic based on matching non-computed attribute values and so it may
    26  // produce strange results with more "extreme" cases, such as a nested set
    27  // block where _all_ attributes are computed.
    28  func ProposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value {
    29  	// If the config and prior are both null, return early here before
    30  	// populating the prior block. The prevents non-null blocks from appearing
    31  	// the proposed state value.
    32  	if config.IsNull() && prior.IsNull() {
    33  		return prior
    34  	}
    35  
    36  	if prior.IsNull() {
    37  		// In this case, we will construct a synthetic prior value that is
    38  		// similar to the result of decoding an empty configuration block,
    39  		// which simplifies our handling of the top-level attributes/blocks
    40  		// below by giving us one non-null level of object to pull values from.
    41  		//
    42  		// "All attributes null" happens to be the definition of EmptyValue for
    43  		// a Block, so we can just delegate to that
    44  		prior = schema.EmptyValue()
    45  	}
    46  	return proposedNew(schema, prior, config)
    47  }
    48  
    49  // PlannedDataResourceObject is similar to proposedNewBlock but tailored for
    50  // planning data resources in particular. Specifically, it replaces the values
    51  // of any Computed attributes not set in the configuration with an unknown
    52  // value, which serves as a placeholder for a value to be filled in by the
    53  // provider when the data resource is finally read.
    54  //
    55  // Data resources are different because the planning of them is handled
    56  // entirely within Terraform Core and not subject to customization by the
    57  // provider. This function is, in effect, producing an equivalent result to
    58  // passing the proposedNewBlock result into a provider's PlanResourceChange
    59  // function, assuming a fixed implementation of PlanResourceChange that just
    60  // fills in unknown values as needed.
    61  func PlannedDataResourceObject(schema *configschema.Block, config cty.Value) cty.Value {
    62  	// Our trick here is to run the proposedNewBlock logic with an
    63  	// entirely-unknown prior value. Because of cty's unknown short-circuit
    64  	// behavior, any operation on prior returns another unknown, and so
    65  	// unknown values propagate into all of the parts of the resulting value
    66  	// that would normally be filled in by preserving the prior state.
    67  	prior := cty.UnknownVal(schema.ImpliedType())
    68  	return proposedNew(schema, prior, config)
    69  }
    70  
    71  func proposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value {
    72  	if config.IsNull() || !config.IsKnown() {
    73  		// A block config should never be null at this point. The only nullable
    74  		// block type is NestingSingle, which will return early before coming
    75  		// back here. We'll allow the null here anyway to free callers from
    76  		// needing to specifically check for these cases, and any mismatch will
    77  		// be caught in validation, so just take the prior value rather than
    78  		// the invalid null.
    79  		return prior
    80  	}
    81  
    82  	if (!prior.Type().IsObjectType()) || (!config.Type().IsObjectType()) {
    83  		panic("ProposedNew only supports object-typed values")
    84  	}
    85  
    86  	// From this point onwards, we can assume that both values are non-null
    87  	// object types, and that the config value itself is known (though it
    88  	// may contain nested values that are unknown.)
    89  	newAttrs := proposedNewAttributes(schema.Attributes, prior, config)
    90  
    91  	// Merging nested blocks is a little more complex, since we need to
    92  	// correlate blocks between both objects and then recursively propose
    93  	// a new object for each. The correlation logic depends on the nesting
    94  	// mode for each block type.
    95  	for name, blockType := range schema.BlockTypes {
    96  		priorV := prior.GetAttr(name)
    97  		configV := config.GetAttr(name)
    98  		newAttrs[name] = proposedNewNestedBlock(blockType, priorV, configV)
    99  	}
   100  
   101  	return cty.ObjectVal(newAttrs)
   102  }
   103  
   104  // proposedNewBlockOrObject dispatched the schema to either ProposedNew or
   105  // proposedNewObjectAttributes depending on the given type.
   106  func proposedNewBlockOrObject(schema nestedSchema, prior, config cty.Value) cty.Value {
   107  	switch schema := schema.(type) {
   108  	case *configschema.Block:
   109  		return ProposedNew(schema, prior, config)
   110  	case *configschema.Object:
   111  		return proposedNewObjectAttributes(schema, prior, config)
   112  	default:
   113  		panic(fmt.Sprintf("unexpected schema type %T", schema))
   114  	}
   115  }
   116  
   117  func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty.Value) cty.Value {
   118  	// The only time we should encounter an entirely unknown block is from the
   119  	// use of dynamic with an unknown for_each expression.
   120  	if !config.IsKnown() {
   121  		return config
   122  	}
   123  
   124  	newV := config
   125  
   126  	switch schema.Nesting {
   127  	case configschema.NestingSingle:
   128  		// A NestingSingle configuration block value can be null, and since it
   129  		// cannot be computed we can always take the configuration value.
   130  		if config.IsNull() {
   131  			break
   132  		}
   133  
   134  		// Otherwise use the same assignment rules as NestingGroup
   135  		fallthrough
   136  	case configschema.NestingGroup:
   137  		newV = ProposedNew(&schema.Block, prior, config)
   138  
   139  	case configschema.NestingList:
   140  		newV = proposedNewNestingList(&schema.Block, prior, config)
   141  
   142  	case configschema.NestingMap:
   143  		newV = proposedNewNestingMap(&schema.Block, prior, config)
   144  
   145  	case configschema.NestingSet:
   146  		newV = proposedNewNestingSet(&schema.Block, prior, config)
   147  
   148  	default:
   149  		// Should never happen, since the above cases are comprehensive.
   150  		panic(fmt.Sprintf("unsupported block nesting mode %s", schema.Nesting))
   151  	}
   152  
   153  	return newV
   154  }
   155  
   156  func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value) cty.Value {
   157  	// if the config isn't known at all, then we must use that value
   158  	if !config.IsKnown() {
   159  		return config
   160  	}
   161  
   162  	// Even if the config is null or empty, we will be using this default value.
   163  	newV := config
   164  
   165  	switch schema.Nesting {
   166  	case configschema.NestingSingle:
   167  		// If the config is null, we already have our value. If the attribute
   168  		// is optional+computed, we won't reach this branch with a null value
   169  		// since the computed case would have been taken.
   170  		if config.IsNull() {
   171  			break
   172  		}
   173  
   174  		newV = proposedNewObjectAttributes(schema, prior, config)
   175  
   176  	case configschema.NestingList:
   177  		newV = proposedNewNestingList(schema, prior, config)
   178  
   179  	case configschema.NestingMap:
   180  		newV = proposedNewNestingMap(schema, prior, config)
   181  
   182  	case configschema.NestingSet:
   183  		newV = proposedNewNestingSet(schema, prior, config)
   184  
   185  	default:
   186  		// Should never happen, since the above cases are comprehensive.
   187  		panic(fmt.Sprintf("unsupported attribute nesting mode %s", schema.Nesting))
   188  	}
   189  
   190  	return newV
   191  }
   192  
   193  func proposedNewNestingList(schema nestedSchema, prior, config cty.Value) cty.Value {
   194  	newV := config
   195  
   196  	// Nested blocks are correlated by index.
   197  	configVLen := 0
   198  	if !config.IsNull() {
   199  		configVLen = config.LengthInt()
   200  	}
   201  	if configVLen > 0 {
   202  		newVals := make([]cty.Value, 0, configVLen)
   203  		for it := config.ElementIterator(); it.Next(); {
   204  			idx, configEV := it.Element()
   205  			if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) {
   206  				// If there is no corresponding prior element then
   207  				// we just take the config value as-is.
   208  				newVals = append(newVals, configEV)
   209  				continue
   210  			}
   211  			priorEV := prior.Index(idx)
   212  
   213  			newVals = append(newVals, proposedNewBlockOrObject(schema, priorEV, configEV))
   214  		}
   215  		// Despite the name, a NestingList might also be a tuple, if
   216  		// its nested schema contains dynamically-typed attributes.
   217  		if config.Type().IsTupleType() {
   218  			newV = cty.TupleVal(newVals)
   219  		} else {
   220  			newV = cty.ListVal(newVals)
   221  		}
   222  	}
   223  
   224  	return newV
   225  }
   226  
   227  func proposedNewNestingMap(schema nestedSchema, prior, config cty.Value) cty.Value {
   228  	newV := config
   229  
   230  	newVals := map[string]cty.Value{}
   231  
   232  	if config.IsNull() || !config.IsKnown() || config.LengthInt() == 0 {
   233  		// We already assigned newVal and there's nothing to compare in
   234  		// config.
   235  		return newV
   236  	}
   237  	cfgMap := config.AsValueMap()
   238  
   239  	// prior may be null or empty
   240  	priorMap := map[string]cty.Value{}
   241  	if !prior.IsNull() && prior.IsKnown() && prior.LengthInt() > 0 {
   242  		priorMap = prior.AsValueMap()
   243  	}
   244  
   245  	for name, configEV := range cfgMap {
   246  		priorEV, inPrior := priorMap[name]
   247  		if !inPrior {
   248  			// If there is no corresponding prior element then
   249  			// we just take the config value as-is.
   250  			newVals[name] = configEV
   251  			continue
   252  		}
   253  
   254  		newVals[name] = proposedNewBlockOrObject(schema, priorEV, configEV)
   255  	}
   256  
   257  	// The value must leave as the same type it came in as
   258  	switch {
   259  	case config.Type().IsObjectType():
   260  		// Although we call the nesting mode "map", we actually use
   261  		// object values so that elements might have different types
   262  		// in case of dynamically-typed attributes.
   263  		newV = cty.ObjectVal(newVals)
   264  	default:
   265  		newV = cty.MapVal(newVals)
   266  	}
   267  
   268  	return newV
   269  }
   270  
   271  func proposedNewNestingSet(schema nestedSchema, prior, config cty.Value) cty.Value {
   272  	if !config.Type().IsSetType() {
   273  		panic("configschema.NestingSet value is not a set as expected")
   274  	}
   275  
   276  	newV := config
   277  	if !config.IsKnown() || config.IsNull() || config.LengthInt() == 0 {
   278  		return newV
   279  	}
   280  
   281  	var priorVals []cty.Value
   282  	if prior.IsKnown() && !prior.IsNull() {
   283  		priorVals = prior.AsValueSlice()
   284  	}
   285  
   286  	var newVals []cty.Value
   287  	// track which prior elements have been used
   288  	used := make([]bool, len(priorVals))
   289  
   290  	for _, configEV := range config.AsValueSlice() {
   291  		var priorEV cty.Value
   292  		for i, priorCmp := range priorVals {
   293  			if used[i] {
   294  				continue
   295  			}
   296  
   297  			// It is possible that multiple prior elements could be valid
   298  			// matches for a configuration value, in which case we will end up
   299  			// picking the first match encountered (but it will always be
   300  			// consistent due to cty's iteration order). Because configured set
   301  			// elements must also be entirely unique in order to be included in
   302  			// the set, these matches either will not matter because they only
   303  			// differ by computed values, or could not have come from a valid
   304  			// config with all unique set elements.
   305  			if validPriorFromConfig(schema, priorCmp, configEV) {
   306  				priorEV = priorCmp
   307  				used[i] = true
   308  				break
   309  			}
   310  		}
   311  
   312  		if priorEV == cty.NilVal {
   313  			priorEV = cty.NullVal(config.Type().ElementType())
   314  		}
   315  
   316  		newVals = append(newVals, proposedNewBlockOrObject(schema, priorEV, configEV))
   317  	}
   318  
   319  	return cty.SetVal(newVals)
   320  }
   321  
   322  func proposedNewObjectAttributes(schema *configschema.Object, prior, config cty.Value) cty.Value {
   323  	if config.IsNull() {
   324  		return config
   325  	}
   326  
   327  	return cty.ObjectVal(proposedNewAttributes(schema.Attributes, prior, config))
   328  }
   329  
   330  func proposedNewAttributes(attrs map[string]*configschema.Attribute, prior, config cty.Value) map[string]cty.Value {
   331  	newAttrs := make(map[string]cty.Value, len(attrs))
   332  	for name, attr := range attrs {
   333  		var priorV cty.Value
   334  		if prior.IsNull() {
   335  			priorV = cty.NullVal(prior.Type().AttributeType(name))
   336  		} else {
   337  			priorV = prior.GetAttr(name)
   338  		}
   339  
   340  		configV := config.GetAttr(name)
   341  
   342  		var newV cty.Value
   343  		switch {
   344  		// required isn't considered when constructing the plan, so attributes
   345  		// are essentially either computed or not computed. In the case of
   346  		// optional+computed, they are only computed when there is no
   347  		// configuration.
   348  		case attr.Computed && configV.IsNull():
   349  			// configV will always be null in this case, by definition.
   350  			// priorV may also be null, but that's okay.
   351  			newV = priorV
   352  
   353  			// the exception to the above is that if the config is optional and
   354  			// the _prior_ value contains non-computed values, we can infer
   355  			// that the config must have been non-null previously.
   356  			if optionalValueNotComputable(attr, priorV) {
   357  				newV = configV
   358  			}
   359  
   360  		case attr.NestedType != nil:
   361  			// For non-computed NestedType attributes, we need to descend
   362  			// into the individual nested attributes to build the final
   363  			// value, unless the entire nested attribute is unknown.
   364  			newV = proposedNewNestedType(attr.NestedType, priorV, configV)
   365  		default:
   366  			// For non-computed attributes, we always take the config value,
   367  			// even if it is null. If it's _required_ then null values
   368  			// should've been caught during an earlier validation step, and
   369  			// so we don't really care about that here.
   370  			newV = configV
   371  		}
   372  		newAttrs[name] = newV
   373  	}
   374  	return newAttrs
   375  }
   376  
   377  // nestedSchema is used as a generic container for either a
   378  // *configschema.Object, or *configschema.Block.
   379  type nestedSchema interface {
   380  	AttributeByPath(cty.Path) *configschema.Attribute
   381  }
   382  
   383  // optionalValueNotComputable is used to check if an object in state must
   384  // have at least partially come from configuration. If the prior value has any
   385  // non-null attributes which are not computed in the schema, then we know there
   386  // was previously a configuration value which set those.
   387  //
   388  // This is used when the configuration contains a null optional+computed value,
   389  // and we want to know if we should plan to send the null value or the prior
   390  // state.
   391  func optionalValueNotComputable(schema *configschema.Attribute, val cty.Value) bool {
   392  	if !schema.Optional {
   393  		return false
   394  	}
   395  
   396  	// We must have a NestedType for complex nested attributes in order
   397  	// to find nested computed values in the first place.
   398  	if schema.NestedType == nil {
   399  		return false
   400  	}
   401  
   402  	foundNonComputedAttr := false
   403  	cty.Walk(val, func(path cty.Path, v cty.Value) (bool, error) {
   404  		if v.IsNull() {
   405  			return true, nil
   406  		}
   407  
   408  		attr := schema.NestedType.AttributeByPath(path)
   409  		if attr == nil {
   410  			return true, nil
   411  		}
   412  
   413  		if !attr.Computed {
   414  			foundNonComputedAttr = true
   415  			return false, nil
   416  		}
   417  		return true, nil
   418  	})
   419  
   420  	return foundNonComputedAttr
   421  }
   422  
   423  // validPriorFromConfig returns true if the prior object could have been
   424  // derived from the configuration. We do this by walking the prior value to
   425  // determine if it is a valid superset of the config, and only computable
   426  // values have been added. This function is only used to correlated
   427  // configuration with possible valid prior values within sets.
   428  func validPriorFromConfig(schema nestedSchema, prior, config cty.Value) bool {
   429  	if config.RawEquals(prior) {
   430  		return true
   431  	}
   432  
   433  	// error value to halt the walk
   434  	stop := errors.New("stop")
   435  
   436  	valid := true
   437  	cty.Walk(prior, func(path cty.Path, priorV cty.Value) (bool, error) {
   438  		configV, err := path.Apply(config)
   439  		if err != nil {
   440  			// most likely dynamic objects with different types
   441  			valid = false
   442  			return false, stop
   443  		}
   444  
   445  		// we don't need to know the schema if both are equal
   446  		if configV.RawEquals(priorV) {
   447  			// we know they are equal, so no need to descend further
   448  			return false, nil
   449  		}
   450  
   451  		// We can't descend into nested sets to correlate configuration, so the
   452  		// overall values must be equal.
   453  		if configV.Type().IsSetType() {
   454  			valid = false
   455  			return false, stop
   456  		}
   457  
   458  		attr := schema.AttributeByPath(path)
   459  		if attr == nil {
   460  			// Not at a schema attribute, so we can continue until we find leaf
   461  			// attributes.
   462  			return true, nil
   463  		}
   464  
   465  		// If we have nested object attributes we'll be descending into those
   466  		// to compare the individual values and determine why this level is not
   467  		// equal
   468  		if attr.NestedType != nil {
   469  			return true, nil
   470  		}
   471  
   472  		// This is a leaf attribute, so it must be computed in order to differ
   473  		// from config.
   474  		if !attr.Computed {
   475  			valid = false
   476  			return false, stop
   477  		}
   478  
   479  		// And if it is computed, the config must be null to allow a change.
   480  		if !configV.IsNull() {
   481  			valid = false
   482  			return false, stop
   483  		}
   484  
   485  		// We sill stop here. The cty value could be far larger, but this was
   486  		// the last level of prescribed schema.
   487  		return false, nil
   488  	})
   489  
   490  	return valid
   491  }