github.com/opentofu/opentofu@v1.7.1/internal/plans/objchange/compatible.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  	"fmt"
    10  	"strconv"
    11  
    12  	"github.com/zclconf/go-cty/cty"
    13  	"github.com/zclconf/go-cty/cty/convert"
    14  
    15  	"github.com/opentofu/opentofu/internal/configs/configschema"
    16  	"github.com/opentofu/opentofu/internal/lang/marks"
    17  )
    18  
    19  // AssertObjectCompatible checks whether the given "actual" value is a valid
    20  // completion of the possibly-partially-unknown "planned" value.
    21  //
    22  // This means that any known leaf value in "planned" must be equal to the
    23  // corresponding value in "actual", and various other similar constraints.
    24  //
    25  // Any inconsistencies are reported by returning a non-zero number of errors.
    26  // These errors are usually (but not necessarily) cty.PathError values
    27  // referring to a particular nested value within the "actual" value.
    28  //
    29  // The two values must have types that conform to the given schema's implied
    30  // type, or this function will panic.
    31  func AssertObjectCompatible(schema *configschema.Block, planned, actual cty.Value) []error {
    32  	return assertObjectCompatible(schema, planned, actual, nil)
    33  }
    34  
    35  func assertObjectCompatible(schema *configschema.Block, planned, actual cty.Value, path cty.Path) []error {
    36  	var errs []error
    37  	var atRoot string
    38  	if len(path) == 0 {
    39  		atRoot = "Root object "
    40  	}
    41  
    42  	if planned.IsNull() && !actual.IsNull() {
    43  		errs = append(errs, path.NewErrorf(fmt.Sprintf("%swas absent, but now present", atRoot)))
    44  		return errs
    45  	}
    46  	if actual.IsNull() && !planned.IsNull() {
    47  		errs = append(errs, path.NewErrorf(fmt.Sprintf("%swas present, but now absent", atRoot)))
    48  		return errs
    49  	}
    50  	if planned.IsNull() {
    51  		// No further checks possible if both values are null
    52  		return errs
    53  	}
    54  
    55  	for name, attrS := range schema.Attributes {
    56  		plannedV := planned.GetAttr(name)
    57  		actualV := actual.GetAttr(name)
    58  
    59  		path := append(path, cty.GetAttrStep{Name: name})
    60  
    61  		// Unmark values here before checking value assertions,
    62  		// but save the marks so we can see if we should supress
    63  		// exposing a value through errors
    64  		unmarkedActualV, marksA := actualV.UnmarkDeep()
    65  		unmarkedPlannedV, marksP := plannedV.UnmarkDeep()
    66  		_, isSensitiveActual := marksA[marks.Sensitive]
    67  		_, isSensitivePlanned := marksP[marks.Sensitive]
    68  
    69  		moreErrs := assertValueCompatible(unmarkedPlannedV, unmarkedActualV, path)
    70  		if attrS.Sensitive || isSensitiveActual || isSensitivePlanned {
    71  			if len(moreErrs) > 0 {
    72  				// Use a vague placeholder message instead, to avoid disclosing
    73  				// sensitive information.
    74  				errs = append(errs, path.NewErrorf("inconsistent values for sensitive attribute"))
    75  			}
    76  		} else {
    77  			errs = append(errs, moreErrs...)
    78  		}
    79  	}
    80  	for name, blockS := range schema.BlockTypes {
    81  		plannedV, _ := planned.GetAttr(name).Unmark()
    82  		actualV, _ := actual.GetAttr(name).Unmark()
    83  
    84  		path := append(path, cty.GetAttrStep{Name: name})
    85  		switch blockS.Nesting {
    86  		case configschema.NestingSingle, configschema.NestingGroup:
    87  			// If an unknown block placeholder was present then the placeholder
    88  			// may have expanded out into zero blocks, which is okay.
    89  			if !plannedV.IsKnown() && actualV.IsNull() {
    90  				continue
    91  			}
    92  			moreErrs := assertObjectCompatible(&blockS.Block, plannedV, actualV, path)
    93  			errs = append(errs, moreErrs...)
    94  		case configschema.NestingList:
    95  			// A NestingList might either be a list or a tuple, depending on
    96  			// whether there are dynamically-typed attributes inside. However,
    97  			// both support a similar-enough API that we can treat them the
    98  			// same for our purposes here.
    99  			if !plannedV.IsKnown() || !actualV.IsKnown() || plannedV.IsNull() || actualV.IsNull() {
   100  				continue
   101  			}
   102  
   103  			plannedL := plannedV.LengthInt()
   104  			actualL := actualV.LengthInt()
   105  			if plannedL != actualL {
   106  				errs = append(errs, path.NewErrorf("block count changed from %d to %d", plannedL, actualL))
   107  				continue
   108  			}
   109  			for it := plannedV.ElementIterator(); it.Next(); {
   110  				idx, plannedEV := it.Element()
   111  				if !actualV.HasIndex(idx).True() {
   112  					continue
   113  				}
   114  				actualEV := actualV.Index(idx)
   115  				moreErrs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.IndexStep{Key: idx}))
   116  				errs = append(errs, moreErrs...)
   117  			}
   118  		case configschema.NestingMap:
   119  			// A NestingMap might either be a map or an object, depending on
   120  			// whether there are dynamically-typed attributes inside, but
   121  			// that's decided statically and so both values will have the same
   122  			// kind.
   123  			if plannedV.Type().IsObjectType() {
   124  				plannedAtys := plannedV.Type().AttributeTypes()
   125  				actualAtys := actualV.Type().AttributeTypes()
   126  				for k := range plannedAtys {
   127  					if _, ok := actualAtys[k]; !ok {
   128  						errs = append(errs, path.NewErrorf("block key %q has vanished", k))
   129  						continue
   130  					}
   131  
   132  					plannedEV := plannedV.GetAttr(k)
   133  					actualEV := actualV.GetAttr(k)
   134  					moreErrs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.GetAttrStep{Name: k}))
   135  					errs = append(errs, moreErrs...)
   136  				}
   137  				if plannedV.IsKnown() { // new blocks may appear if unknown blocks were present in the plan
   138  					for k := range actualAtys {
   139  						if _, ok := plannedAtys[k]; !ok {
   140  							errs = append(errs, path.NewErrorf("new block key %q has appeared", k))
   141  							continue
   142  						}
   143  					}
   144  				}
   145  			} else {
   146  				if !plannedV.IsKnown() || plannedV.IsNull() || actualV.IsNull() {
   147  					continue
   148  				}
   149  				plannedL := plannedV.LengthInt()
   150  				actualL := actualV.LengthInt()
   151  				if plannedL != actualL && plannedV.IsKnown() { // new blocks may appear if unknown blocks were persent in the plan
   152  					errs = append(errs, path.NewErrorf("block count changed from %d to %d", plannedL, actualL))
   153  					continue
   154  				}
   155  				for it := plannedV.ElementIterator(); it.Next(); {
   156  					idx, plannedEV := it.Element()
   157  					if !actualV.HasIndex(idx).True() {
   158  						continue
   159  					}
   160  					actualEV := actualV.Index(idx)
   161  					moreErrs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.IndexStep{Key: idx}))
   162  					errs = append(errs, moreErrs...)
   163  				}
   164  			}
   165  		case configschema.NestingSet:
   166  			if !plannedV.IsKnown() || !actualV.IsKnown() || plannedV.IsNull() || actualV.IsNull() {
   167  				continue
   168  			}
   169  
   170  			if !plannedV.IsKnown() {
   171  				// When unknown blocks are present the final number of blocks
   172  				// may be different, either because the unknown set values
   173  				// become equal and are collapsed, or the count is unknown due
   174  				// a dynamic block. Unfortunately this means we can't do our
   175  				// usual checks in this case without generating false
   176  				// negatives.
   177  				continue
   178  			}
   179  
   180  			setErrs := assertSetValuesCompatible(plannedV, actualV, path, func(plannedEV, actualEV cty.Value) bool {
   181  				errs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.IndexStep{Key: actualEV}))
   182  				return len(errs) == 0
   183  			})
   184  			errs = append(errs, setErrs...)
   185  
   186  			// There can be fewer elements in a set after its elements are all
   187  			// known (values that turn out to be equal will coalesce) but the
   188  			// number of elements must never get larger.
   189  			plannedL := plannedV.LengthInt()
   190  			actualL := actualV.LengthInt()
   191  			if plannedL < actualL {
   192  				errs = append(errs, path.NewErrorf("block set length changed from %d to %d", plannedL, actualL))
   193  			}
   194  		default:
   195  			panic(fmt.Sprintf("unsupported nesting mode %s", blockS.Nesting))
   196  		}
   197  	}
   198  	return errs
   199  }
   200  
   201  func assertValueCompatible(planned, actual cty.Value, path cty.Path) []error {
   202  	// NOTE: We don't normally use the GoString rendering of cty.Value in
   203  	// user-facing error messages as a rule, but we make an exception
   204  	// for this function because we expect the user to pass this message on
   205  	// verbatim to the provider development team and so more detail is better.
   206  
   207  	var errs []error
   208  	if planned.Type() == cty.DynamicPseudoType {
   209  		// Anything goes, then
   210  		return errs
   211  	}
   212  	if problems := actual.Type().TestConformance(planned.Type()); len(problems) > 0 {
   213  		errs = append(errs, path.NewErrorf("wrong final value type: %s", convert.MismatchMessage(actual.Type(), planned.Type())))
   214  		// If the types don't match then we can't do any other comparisons,
   215  		// so we bail early.
   216  		return errs
   217  	}
   218  
   219  	if !planned.IsKnown() {
   220  		// We didn't know what were going to end up with during plan, so
   221  		// the final value needs only to match the type and refinements of
   222  		// the unknown value placeholder.
   223  		plannedRng := planned.Range()
   224  		if ok := plannedRng.Includes(actual); ok.IsKnown() && ok.False() {
   225  			errs = append(errs, path.NewErrorf("final value %#v does not conform to planning placeholder %#v", actual, planned))
   226  		}
   227  		return errs
   228  	}
   229  
   230  	if actual.IsNull() {
   231  		if planned.IsNull() {
   232  			return nil
   233  		}
   234  		errs = append(errs, path.NewErrorf("was %#v, but now null", planned))
   235  		return errs
   236  	}
   237  	if planned.IsNull() {
   238  		errs = append(errs, path.NewErrorf("was null, but now %#v", actual))
   239  		return errs
   240  	}
   241  
   242  	ty := planned.Type()
   243  	switch {
   244  
   245  	case !actual.IsKnown():
   246  		errs = append(errs, path.NewErrorf("was known, but now unknown"))
   247  
   248  	case ty.IsPrimitiveType():
   249  		if !actual.Equals(planned).True() {
   250  			errs = append(errs, path.NewErrorf("was %#v, but now %#v", planned, actual))
   251  		}
   252  
   253  	case ty.IsListType() || ty.IsMapType() || ty.IsTupleType():
   254  		for it := planned.ElementIterator(); it.Next(); {
   255  			k, plannedV := it.Element()
   256  			if !actual.HasIndex(k).True() {
   257  				errs = append(errs, path.NewErrorf("element %s has vanished", indexStrForErrors(k)))
   258  				continue
   259  			}
   260  
   261  			actualV := actual.Index(k)
   262  			moreErrs := assertValueCompatible(plannedV, actualV, append(path, cty.IndexStep{Key: k}))
   263  			errs = append(errs, moreErrs...)
   264  		}
   265  
   266  		for it := actual.ElementIterator(); it.Next(); {
   267  			k, _ := it.Element()
   268  			if !planned.HasIndex(k).True() {
   269  				errs = append(errs, path.NewErrorf("new element %s has appeared", indexStrForErrors(k)))
   270  			}
   271  		}
   272  
   273  	case ty.IsObjectType():
   274  		atys := ty.AttributeTypes()
   275  		for name := range atys {
   276  			// Because we already tested that the two values have the same type,
   277  			// we can assume that the same attributes are present in both and
   278  			// focus just on testing their values.
   279  			plannedV := planned.GetAttr(name)
   280  			actualV := actual.GetAttr(name)
   281  			moreErrs := assertValueCompatible(plannedV, actualV, append(path, cty.GetAttrStep{Name: name}))
   282  			errs = append(errs, moreErrs...)
   283  		}
   284  
   285  	case ty.IsSetType():
   286  		// We can't really do anything useful for sets here because changing
   287  		// an unknown element to known changes the identity of the element, and
   288  		// so we can't correlate them properly. However, we will at least check
   289  		// to ensure that the number of elements is consistent, along with
   290  		// the general type-match checks we ran earlier in this function.
   291  		if planned.IsKnown() && !planned.IsNull() && !actual.IsNull() {
   292  
   293  			setErrs := assertSetValuesCompatible(planned, actual, path, func(plannedV, actualV cty.Value) bool {
   294  				errs := assertValueCompatible(plannedV, actualV, append(path, cty.IndexStep{Key: actualV}))
   295  				return len(errs) == 0
   296  			})
   297  			errs = append(errs, setErrs...)
   298  
   299  			// There can be fewer elements in a set after its elements are all
   300  			// known (values that turn out to be equal will coalesce) but the
   301  			// number of elements must never get larger.
   302  
   303  			plannedL := planned.LengthInt()
   304  			actualL := actual.LengthInt()
   305  			if plannedL < actualL {
   306  				errs = append(errs, path.NewErrorf("length changed from %d to %d", plannedL, actualL))
   307  			}
   308  		}
   309  	}
   310  
   311  	return errs
   312  }
   313  
   314  func indexStrForErrors(v cty.Value) string {
   315  	switch v.Type() {
   316  	case cty.Number:
   317  		return v.AsBigFloat().Text('f', -1)
   318  	case cty.String:
   319  		return strconv.Quote(v.AsString())
   320  	default:
   321  		// Should be impossible, since no other index types are allowed!
   322  		return fmt.Sprintf("%#v", v)
   323  	}
   324  }
   325  
   326  // assertSetValuesCompatible checks that each of the elements in a can
   327  // be correlated with at least one equivalent element in b and vice-versa,
   328  // using the given correlation function.
   329  //
   330  // This allows the number of elements in the sets to change as long as all
   331  // elements in both sets can be correlated, making this function safe to use
   332  // with sets that may contain unknown values as long as the unknown case is
   333  // addressed in some reasonable way in the callback function.
   334  //
   335  // The callback always recieves values from set a as its first argument and
   336  // values from set b in its second argument, so it is safe to use with
   337  // non-commutative functions.
   338  //
   339  // As with assertValueCompatible, we assume that the target audience of error
   340  // messages here is a provider developer (via a bug report from a user) and so
   341  // we intentionally violate our usual rule of keeping cty implementation
   342  // details out of error messages.
   343  func assertSetValuesCompatible(planned, actual cty.Value, path cty.Path, f func(aVal, bVal cty.Value) bool) []error {
   344  	a := planned
   345  	b := actual
   346  
   347  	// Our methodology here is a little tricky, to deal with the fact that
   348  	// it's impossible to directly correlate two non-equal set elements because
   349  	// they don't have identities separate from their values.
   350  	// The approach is to count the number of equivalent elements each element
   351  	// of a has in b and vice-versa, and then return true only if each element
   352  	// in both sets has at least one equivalent.
   353  	as := a.AsValueSlice()
   354  	bs := b.AsValueSlice()
   355  	aeqs := make([]bool, len(as))
   356  	beqs := make([]bool, len(bs))
   357  	for ai, av := range as {
   358  		for bi, bv := range bs {
   359  			if f(av, bv) {
   360  				aeqs[ai] = true
   361  				beqs[bi] = true
   362  			}
   363  		}
   364  	}
   365  
   366  	var errs []error
   367  	for i, eq := range aeqs {
   368  		if !eq {
   369  			errs = append(errs, path.NewErrorf("planned set element %#v does not correlate with any element in actual", as[i]))
   370  		}
   371  	}
   372  	if len(errs) > 0 {
   373  		// Exit early since otherwise we're likely to generate duplicate
   374  		// error messages from the other perspective in the subsequent loop.
   375  		return errs
   376  	}
   377  	for i, eq := range beqs {
   378  		if !eq {
   379  			errs = append(errs, path.NewErrorf("actual set element %#v does not correlate with any element in plan", bs[i]))
   380  		}
   381  	}
   382  	return errs
   383  }