github.com/opentofu/opentofu@v1.7.1/internal/plans/objchange/objchange.go (about)

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