github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/plans/objchange/compatible.go (about)

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