github.com/hashicorp/terraform-plugin-sdk@v1.17.2/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/hashicorp/terraform-plugin-sdk/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  	if planned.IsNull() && !actual.IsNull() {
    32  		errs = append(errs, path.NewErrorf("was absent, but now present"))
    33  		return errs
    34  	}
    35  	if actual.IsNull() && !planned.IsNull() {
    36  		errs = append(errs, path.NewErrorf("was present, but now absent"))
    37  		return errs
    38  	}
    39  	if planned.IsNull() {
    40  		// No further checks possible if both values are null
    41  		return errs
    42  	}
    43  
    44  	for name, attrS := range schema.Attributes {
    45  		plannedV := planned.GetAttr(name)
    46  		actualV := actual.GetAttr(name)
    47  
    48  		path := append(path, cty.GetAttrStep{Name: name})
    49  		moreErrs := assertValueCompatible(plannedV, actualV, path)
    50  		if attrS.Sensitive {
    51  			if len(moreErrs) > 0 {
    52  				// Use a vague placeholder message instead, to avoid disclosing
    53  				// sensitive information.
    54  				errs = append(errs, path.NewErrorf("inconsistent values for sensitive attribute"))
    55  			}
    56  		} else {
    57  			errs = append(errs, moreErrs...)
    58  		}
    59  	}
    60  	for name, blockS := range schema.BlockTypes {
    61  		plannedV := planned.GetAttr(name)
    62  		actualV := actual.GetAttr(name)
    63  
    64  		// As a special case, if there were any blocks whose leaf attributes
    65  		// are all unknown then we assume (possibly incorrectly) that the
    66  		// HCL dynamic block extension is in use with an unknown for_each
    67  		// argument, and so we will do looser validation here that allows
    68  		// for those blocks to have expanded into a different number of blocks
    69  		// if the for_each value is now known.
    70  		maybeUnknownBlocks := couldHaveUnknownBlockPlaceholder(plannedV, blockS, false)
    71  
    72  		path := append(path, cty.GetAttrStep{Name: name})
    73  		switch blockS.Nesting {
    74  		case configschema.NestingSingle, configschema.NestingGroup:
    75  			// If an unknown block placeholder was present then the placeholder
    76  			// may have expanded out into zero blocks, which is okay.
    77  			if maybeUnknownBlocks && actualV.IsNull() {
    78  				continue
    79  			}
    80  			moreErrs := assertObjectCompatible(&blockS.Block, plannedV, actualV, path)
    81  			errs = append(errs, moreErrs...)
    82  		case configschema.NestingList:
    83  			// A NestingList might either be a list or a tuple, depending on
    84  			// whether there are dynamically-typed attributes inside. However,
    85  			// both support a similar-enough API that we can treat them the
    86  			// same for our purposes here.
    87  			if !plannedV.IsKnown() || !actualV.IsKnown() || plannedV.IsNull() || actualV.IsNull() {
    88  				continue
    89  			}
    90  
    91  			if maybeUnknownBlocks {
    92  				// When unknown blocks are present the final blocks may be
    93  				// at different indices than the planned blocks, so unfortunately
    94  				// we can't do our usual checks in this case without generating
    95  				// false negatives.
    96  				continue
    97  			}
    98  
    99  			plannedL := plannedV.LengthInt()
   100  			actualL := actualV.LengthInt()
   101  			if plannedL != actualL {
   102  				errs = append(errs, path.NewErrorf("block count changed from %d to %d", plannedL, actualL))
   103  				continue
   104  			}
   105  			for it := plannedV.ElementIterator(); it.Next(); {
   106  				idx, plannedEV := it.Element()
   107  				if !actualV.HasIndex(idx).True() {
   108  					continue
   109  				}
   110  				actualEV := actualV.Index(idx)
   111  				moreErrs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.IndexStep{Key: idx}))
   112  				errs = append(errs, moreErrs...)
   113  			}
   114  		case configschema.NestingMap:
   115  			// A NestingMap might either be a map or an object, depending on
   116  			// whether there are dynamically-typed attributes inside, but
   117  			// that's decided statically and so both values will have the same
   118  			// kind.
   119  			if plannedV.Type().IsObjectType() {
   120  				plannedAtys := plannedV.Type().AttributeTypes()
   121  				actualAtys := actualV.Type().AttributeTypes()
   122  				for k := range plannedAtys {
   123  					if _, ok := actualAtys[k]; !ok {
   124  						errs = append(errs, path.NewErrorf("block key %q has vanished", k))
   125  						continue
   126  					}
   127  
   128  					plannedEV := plannedV.GetAttr(k)
   129  					actualEV := actualV.GetAttr(k)
   130  					moreErrs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.GetAttrStep{Name: k}))
   131  					errs = append(errs, moreErrs...)
   132  				}
   133  				if !maybeUnknownBlocks { // new blocks may appear if unknown blocks were present in the plan
   134  					for k := range actualAtys {
   135  						if _, ok := plannedAtys[k]; !ok {
   136  							errs = append(errs, path.NewErrorf("new block key %q has appeared", k))
   137  							continue
   138  						}
   139  					}
   140  				}
   141  			} else {
   142  				if !plannedV.IsKnown() || plannedV.IsNull() || actualV.IsNull() {
   143  					continue
   144  				}
   145  				plannedL := plannedV.LengthInt()
   146  				actualL := actualV.LengthInt()
   147  				if plannedL != actualL && !maybeUnknownBlocks { // new blocks may appear if unknown blocks were persent in the plan
   148  					errs = append(errs, path.NewErrorf("block count changed from %d to %d", plannedL, actualL))
   149  					continue
   150  				}
   151  				for it := plannedV.ElementIterator(); it.Next(); {
   152  					idx, plannedEV := it.Element()
   153  					if !actualV.HasIndex(idx).True() {
   154  						continue
   155  					}
   156  					actualEV := actualV.Index(idx)
   157  					moreErrs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.IndexStep{Key: idx}))
   158  					errs = append(errs, moreErrs...)
   159  				}
   160  			}
   161  		case configschema.NestingSet:
   162  			if !plannedV.IsKnown() || !actualV.IsKnown() || plannedV.IsNull() || actualV.IsNull() {
   163  				continue
   164  			}
   165  
   166  			setErrs := assertSetValuesCompatible(plannedV, actualV, path, func(plannedEV, actualEV cty.Value) bool {
   167  				errs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.IndexStep{Key: actualEV}))
   168  				return len(errs) == 0
   169  			})
   170  			errs = append(errs, setErrs...)
   171  
   172  			if maybeUnknownBlocks {
   173  				// When unknown blocks are present the final number of blocks
   174  				// may be different, either because the unknown set values
   175  				// become equal and are collapsed, or the count is unknown due
   176  				// a dynamic block. Unfortunately this means we can't do our
   177  				// usual checks in this case without generating false
   178  				// negatives.
   179  				continue
   180  			}
   181  
   182  			// There can be fewer elements in a set after its elements are all
   183  			// known (values that turn out to be equal will coalesce) but the
   184  			// number of elements must never get larger.
   185  			plannedL := plannedV.LengthInt()
   186  			actualL := actualV.LengthInt()
   187  			if plannedL < actualL {
   188  				errs = append(errs, path.NewErrorf("block set length changed from %d to %d", plannedL, actualL))
   189  			}
   190  		default:
   191  			panic(fmt.Sprintf("unsupported nesting mode %s", blockS.Nesting))
   192  		}
   193  	}
   194  	return errs
   195  }
   196  
   197  func assertValueCompatible(planned, actual cty.Value, path cty.Path) []error {
   198  	// NOTE: We don't normally use the GoString rendering of cty.Value in
   199  	// user-facing error messages as a rule, but we make an exception
   200  	// for this function because we expect the user to pass this message on
   201  	// verbatim to the provider development team and so more detail is better.
   202  
   203  	var errs []error
   204  	if planned.Type() == cty.DynamicPseudoType {
   205  		// Anything goes, then
   206  		return errs
   207  	}
   208  	if problems := planned.Type().TestConformance(actual.Type()); len(problems) > 0 {
   209  		errs = append(errs, path.NewErrorf("wrong final value type: %s", convert.MismatchMessage(actual.Type(), planned.Type())))
   210  		// If the types don't match then we can't do any other comparisons,
   211  		// so we bail early.
   212  		return errs
   213  	}
   214  
   215  	if !planned.IsKnown() {
   216  		// We didn't know what were going to end up with during plan, so
   217  		// anything goes during apply.
   218  		return errs
   219  	}
   220  
   221  	if actual.IsNull() {
   222  		if planned.IsNull() {
   223  			return nil
   224  		}
   225  		errs = append(errs, path.NewErrorf("was %#v, but now null", planned))
   226  		return errs
   227  	}
   228  	if planned.IsNull() {
   229  		errs = append(errs, path.NewErrorf("was null, but now %#v", actual))
   230  		return errs
   231  	}
   232  
   233  	ty := planned.Type()
   234  	switch {
   235  
   236  	case !actual.IsKnown():
   237  		errs = append(errs, path.NewErrorf("was known, but now unknown"))
   238  
   239  	case ty.IsPrimitiveType():
   240  		if !actual.Equals(planned).True() {
   241  			errs = append(errs, path.NewErrorf("was %#v, but now %#v", planned, actual))
   242  		}
   243  
   244  	case ty.IsListType() || ty.IsMapType() || ty.IsTupleType():
   245  		for it := planned.ElementIterator(); it.Next(); {
   246  			k, plannedV := it.Element()
   247  			if !actual.HasIndex(k).True() {
   248  				errs = append(errs, path.NewErrorf("element %s has vanished", indexStrForErrors(k)))
   249  				continue
   250  			}
   251  
   252  			actualV := actual.Index(k)
   253  			moreErrs := assertValueCompatible(plannedV, actualV, append(path, cty.IndexStep{Key: k}))
   254  			errs = append(errs, moreErrs...)
   255  		}
   256  
   257  		for it := actual.ElementIterator(); it.Next(); {
   258  			k, _ := it.Element()
   259  			if !planned.HasIndex(k).True() {
   260  				errs = append(errs, path.NewErrorf("new element %s has appeared", indexStrForErrors(k)))
   261  			}
   262  		}
   263  
   264  	case ty.IsObjectType():
   265  		atys := ty.AttributeTypes()
   266  		for name := range atys {
   267  			// Because we already tested that the two values have the same type,
   268  			// we can assume that the same attributes are present in both and
   269  			// focus just on testing their values.
   270  			plannedV := planned.GetAttr(name)
   271  			actualV := actual.GetAttr(name)
   272  			moreErrs := assertValueCompatible(plannedV, actualV, append(path, cty.GetAttrStep{Name: name}))
   273  			errs = append(errs, moreErrs...)
   274  		}
   275  
   276  	case ty.IsSetType():
   277  		// We can't really do anything useful for sets here because changing
   278  		// an unknown element to known changes the identity of the element, and
   279  		// so we can't correlate them properly. However, we will at least check
   280  		// to ensure that the number of elements is consistent, along with
   281  		// the general type-match checks we ran earlier in this function.
   282  		if planned.IsKnown() && !planned.IsNull() && !actual.IsNull() {
   283  
   284  			setErrs := assertSetValuesCompatible(planned, actual, path, func(plannedV, actualV cty.Value) bool {
   285  				errs := assertValueCompatible(plannedV, actualV, append(path, cty.IndexStep{Key: actualV}))
   286  				return len(errs) == 0
   287  			})
   288  			errs = append(errs, setErrs...)
   289  
   290  			// There can be fewer elements in a set after its elements are all
   291  			// known (values that turn out to be equal will coalesce) but the
   292  			// number of elements must never get larger.
   293  
   294  			plannedL := planned.LengthInt()
   295  			actualL := actual.LengthInt()
   296  			if plannedL < actualL {
   297  				errs = append(errs, path.NewErrorf("length changed from %d to %d", plannedL, actualL))
   298  			}
   299  		}
   300  	}
   301  
   302  	return errs
   303  }
   304  
   305  func indexStrForErrors(v cty.Value) string {
   306  	switch v.Type() {
   307  	case cty.Number:
   308  		return v.AsBigFloat().Text('f', -1)
   309  	case cty.String:
   310  		return strconv.Quote(v.AsString())
   311  	default:
   312  		// Should be impossible, since no other index types are allowed!
   313  		return fmt.Sprintf("%#v", v)
   314  	}
   315  }
   316  
   317  // couldHaveUnknownBlockPlaceholder is a heuristic that recognizes how the
   318  // HCL dynamic block extension behaves when it's asked to expand a block whose
   319  // for_each argument is unknown. In such cases, it generates a single placeholder
   320  // block with all leaf attribute values unknown, and once the for_each
   321  // expression becomes known the placeholder may be replaced with any number
   322  // of blocks, so object compatibility checks would need to be more liberal.
   323  //
   324  // Set "nested" if testing a block that is nested inside a candidate block
   325  // placeholder; this changes the interpretation of there being no blocks of
   326  // a type to allow for there being zero nested blocks.
   327  func couldHaveUnknownBlockPlaceholder(v cty.Value, blockS *configschema.NestedBlock, nested bool) bool {
   328  	switch blockS.Nesting {
   329  	case configschema.NestingSingle, configschema.NestingGroup:
   330  		if nested && v.IsNull() {
   331  			return true // for nested blocks, a single block being unset doesn't disqualify from being an unknown block placeholder
   332  		}
   333  		return couldBeUnknownBlockPlaceholderElement(v, &blockS.Block)
   334  	default:
   335  		// These situations should be impossible for correct providers, but
   336  		// we permit the legacy SDK to produce some incorrect outcomes
   337  		// for compatibility with its existing logic, and so we must be
   338  		// tolerant here.
   339  		if !v.IsKnown() {
   340  			return true
   341  		}
   342  		if v.IsNull() {
   343  			return false // treated as if the list were empty, so we would see zero iterations below
   344  		}
   345  
   346  		// For all other nesting modes, our value should be something iterable.
   347  		for it := v.ElementIterator(); it.Next(); {
   348  			_, ev := it.Element()
   349  			if couldBeUnknownBlockPlaceholderElement(ev, &blockS.Block) {
   350  				return true
   351  			}
   352  		}
   353  
   354  		// Our default changes depending on whether we're testing the candidate
   355  		// block itself or something nested inside of it: zero blocks of a type
   356  		// can never contain a dynamic block placeholder, but a dynamic block
   357  		// placeholder might contain zero blocks of one of its own nested block
   358  		// types, if none were set in the config at all.
   359  		return nested
   360  	}
   361  }
   362  
   363  func couldBeUnknownBlockPlaceholderElement(v cty.Value, schema *configschema.Block) bool {
   364  	if v.IsNull() {
   365  		return false // null value can never be a placeholder element
   366  	}
   367  	if !v.IsKnown() {
   368  		return true // this should never happen for well-behaved providers, but can happen with the legacy SDK opt-outs
   369  	}
   370  	for name := range schema.Attributes {
   371  		av := v.GetAttr(name)
   372  
   373  		// Unknown block placeholders contain only unknown or null attribute
   374  		// values, depending on whether or not a particular attribute was set
   375  		// explicitly inside the content block. Note that this is imprecise:
   376  		// non-placeholders can also match this, so this function can generate
   377  		// false positives.
   378  		if av.IsKnown() && !av.IsNull() {
   379  			return false
   380  		}
   381  	}
   382  	for name, blockS := range schema.BlockTypes {
   383  		if !couldHaveUnknownBlockPlaceholder(v.GetAttr(name), blockS, true) {
   384  			return false
   385  		}
   386  	}
   387  	return true
   388  }
   389  
   390  // assertSetValuesCompatible checks that each of the elements in a can
   391  // be correlated with at least one equivalent element in b and vice-versa,
   392  // using the given correlation function.
   393  //
   394  // This allows the number of elements in the sets to change as long as all
   395  // elements in both sets can be correlated, making this function safe to use
   396  // with sets that may contain unknown values as long as the unknown case is
   397  // addressed in some reasonable way in the callback function.
   398  //
   399  // The callback always recieves values from set a as its first argument and
   400  // values from set b in its second argument, so it is safe to use with
   401  // non-commutative functions.
   402  //
   403  // As with assertValueCompatible, we assume that the target audience of error
   404  // messages here is a provider developer (via a bug report from a user) and so
   405  // we intentionally violate our usual rule of keeping cty implementation
   406  // details out of error messages.
   407  func assertSetValuesCompatible(planned, actual cty.Value, path cty.Path, f func(aVal, bVal cty.Value) bool) []error {
   408  	a := planned
   409  	b := actual
   410  
   411  	// Our methodology here is a little tricky, to deal with the fact that
   412  	// it's impossible to directly correlate two non-equal set elements because
   413  	// they don't have identities separate from their values.
   414  	// The approach is to count the number of equivalent elements each element
   415  	// of a has in b and vice-versa, and then return true only if each element
   416  	// in both sets has at least one equivalent.
   417  	as := a.AsValueSlice()
   418  	bs := b.AsValueSlice()
   419  	aeqs := make([]bool, len(as))
   420  	beqs := make([]bool, len(bs))
   421  	for ai, av := range as {
   422  		for bi, bv := range bs {
   423  			if f(av, bv) {
   424  				aeqs[ai] = true
   425  				beqs[bi] = true
   426  			}
   427  		}
   428  	}
   429  
   430  	var errs []error
   431  	for i, eq := range aeqs {
   432  		if !eq {
   433  			errs = append(errs, path.NewErrorf("planned set element %#v does not correlate with any element in actual", as[i]))
   434  		}
   435  	}
   436  	if len(errs) > 0 {
   437  		// Exit early since otherwise we're likely to generate duplicate
   438  		// error messages from the other perspective in the subsequent loop.
   439  		return errs
   440  	}
   441  	for i, eq := range beqs {
   442  		if !eq {
   443  			errs = append(errs, path.NewErrorf("actual set element %#v does not correlate with any element in plan", bs[i]))
   444  		}
   445  	}
   446  	return errs
   447  }