github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/plans/objchange/compatible.go (about)

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