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

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package objchange
     5  
     6  import (
     7  	"fmt"
     8  
     9  	"github.com/zclconf/go-cty/cty"
    10  
    11  	"github.com/terramate-io/tf/configs/configschema"
    12  )
    13  
    14  // AssertPlanValid checks checks whether a planned new state returned by a
    15  // provider's PlanResourceChange method is suitable to achieve a change
    16  // from priorState to config. It returns a slice with nonzero length if
    17  // any problems are detected. Because problems here indicate bugs in the
    18  // provider that generated the plannedState, they are written with provider
    19  // developers as an audience, rather than end-users.
    20  //
    21  // All of the given values must have the same type and must conform to the
    22  // implied type of the given schema, or this function may panic or produce
    23  // garbage results.
    24  //
    25  // During planning, a provider may only make changes to attributes that are
    26  // null (unset) in the configuration and are marked as "computed" in the
    27  // resource type schema, in order to insert any default values the provider
    28  // may know about. If the default value cannot be determined until apply time,
    29  // the provider can return an unknown value. Providers are forbidden from
    30  // planning a change that disagrees with any non-null argument in the
    31  // configuration.
    32  //
    33  // As a special exception, providers _are_ allowed to provide attribute values
    34  // conflicting with configuration if and only if the planned value exactly
    35  // matches the corresponding attribute value in the prior state. The provider
    36  // can use this to signal that the new value is functionally equivalent to
    37  // the old and thus no change is required.
    38  func AssertPlanValid(schema *configschema.Block, priorState, config, plannedState cty.Value) []error {
    39  	return assertPlanValid(schema, priorState, config, plannedState, nil)
    40  }
    41  
    42  func assertPlanValid(schema *configschema.Block, priorState, config, plannedState cty.Value, path cty.Path) []error {
    43  	var errs []error
    44  	if plannedState.IsNull() && !config.IsNull() {
    45  		errs = append(errs, path.NewErrorf("planned for absence but config wants existence"))
    46  		return errs
    47  	}
    48  	if config.IsNull() && !plannedState.IsNull() {
    49  		errs = append(errs, path.NewErrorf("planned for existence but config wants absence"))
    50  		return errs
    51  	}
    52  	if plannedState.IsNull() {
    53  		// No further checks possible if the planned value is null
    54  		return errs
    55  	}
    56  
    57  	impTy := schema.ImpliedType()
    58  
    59  	// verify attributes
    60  	moreErrs := assertPlannedAttrsValid(schema.Attributes, priorState, config, plannedState, path)
    61  	errs = append(errs, moreErrs...)
    62  
    63  	for name, blockS := range schema.BlockTypes {
    64  		path := append(path, cty.GetAttrStep{Name: name})
    65  		plannedV := plannedState.GetAttr(name)
    66  		configV := config.GetAttr(name)
    67  		priorV := cty.NullVal(impTy.AttributeType(name))
    68  		if !priorState.IsNull() {
    69  			priorV = priorState.GetAttr(name)
    70  		}
    71  		if plannedV.RawEquals(configV) {
    72  			// Easy path: nothing has changed at all
    73  			continue
    74  		}
    75  
    76  		if !configV.IsKnown() {
    77  			// An unknown config block represents a dynamic block where the
    78  			// for_each value is unknown, and therefor cannot be altered by the
    79  			// provider.
    80  			errs = append(errs, path.NewErrorf("planned value %#v for unknown dynamic block", plannedV))
    81  			continue
    82  		}
    83  
    84  		if !plannedV.IsKnown() {
    85  			// Only dynamic configuration can set blocks to unknown, so this is
    86  			// not allowed from the provider. This means that either the config
    87  			// and plan should match, or we have an error where the plan
    88  			// changed the config value, both of which have been checked.
    89  			errs = append(errs, path.NewErrorf("attribute representing nested block must not be unknown itself; set nested attribute values to unknown instead"))
    90  			continue
    91  		}
    92  
    93  		switch blockS.Nesting {
    94  		case configschema.NestingSingle, configschema.NestingGroup:
    95  			moreErrs := assertPlanValid(&blockS.Block, priorV, configV, plannedV, path)
    96  			errs = append(errs, moreErrs...)
    97  		case configschema.NestingList:
    98  			// A NestingList might either be a list or a tuple, depending on
    99  			// whether there are dynamically-typed attributes inside. However,
   100  			// both support a similar-enough API that we can treat them the
   101  			// same for our purposes here.
   102  			if plannedV.IsNull() {
   103  				errs = append(errs, path.NewErrorf("attribute representing a list of nested blocks must be empty to indicate no blocks, not null"))
   104  				continue
   105  			}
   106  
   107  			if configV.IsNull() {
   108  				// Configuration cannot decode a block into a null value, but
   109  				// we could be dealing with a null returned by a legacy
   110  				// provider and inserted via ignore_changes. Fix the value in
   111  				// place so the length can still be compared.
   112  				configV = cty.ListValEmpty(configV.Type().ElementType())
   113  			}
   114  
   115  			plannedL := plannedV.LengthInt()
   116  			configL := configV.LengthInt()
   117  			if plannedL != configL {
   118  				errs = append(errs, path.NewErrorf("block count in plan (%d) disagrees with count in config (%d)", plannedL, configL))
   119  				continue
   120  			}
   121  
   122  			for it := plannedV.ElementIterator(); it.Next(); {
   123  				idx, plannedEV := it.Element()
   124  				path := append(path, cty.IndexStep{Key: idx})
   125  				if !plannedEV.IsKnown() {
   126  					errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead"))
   127  					continue
   128  				}
   129  				if !configV.HasIndex(idx).True() {
   130  					continue // should never happen since we checked the lengths above
   131  				}
   132  				configEV := configV.Index(idx)
   133  				priorEV := cty.NullVal(blockS.ImpliedType())
   134  				if !priorV.IsNull() && priorV.HasIndex(idx).True() {
   135  					priorEV = priorV.Index(idx)
   136  				}
   137  
   138  				moreErrs := assertPlanValid(&blockS.Block, priorEV, configEV, plannedEV, path)
   139  				errs = append(errs, moreErrs...)
   140  			}
   141  		case configschema.NestingMap:
   142  			if plannedV.IsNull() {
   143  				errs = append(errs, path.NewErrorf("attribute representing a map of nested blocks must be empty to indicate no blocks, not null"))
   144  				continue
   145  			}
   146  
   147  			// A NestingMap might either be a map or an object, depending on
   148  			// whether there are dynamically-typed attributes inside, but
   149  			// that's decided statically and so all values will have the same
   150  			// kind.
   151  			if plannedV.Type().IsObjectType() {
   152  				plannedAtys := plannedV.Type().AttributeTypes()
   153  				configAtys := configV.Type().AttributeTypes()
   154  				for k := range plannedAtys {
   155  					if _, ok := configAtys[k]; !ok {
   156  						errs = append(errs, path.NewErrorf("block key %q from plan is not present in config", k))
   157  						continue
   158  					}
   159  					path := append(path, cty.GetAttrStep{Name: k})
   160  
   161  					plannedEV := plannedV.GetAttr(k)
   162  					if !plannedEV.IsKnown() {
   163  						errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead"))
   164  						continue
   165  					}
   166  					configEV := configV.GetAttr(k)
   167  					priorEV := cty.NullVal(blockS.ImpliedType())
   168  					if !priorV.IsNull() && priorV.Type().HasAttribute(k) {
   169  						priorEV = priorV.GetAttr(k)
   170  					}
   171  					moreErrs := assertPlanValid(&blockS.Block, priorEV, configEV, plannedEV, path)
   172  					errs = append(errs, moreErrs...)
   173  				}
   174  				for k := range configAtys {
   175  					if _, ok := plannedAtys[k]; !ok {
   176  						errs = append(errs, path.NewErrorf("block key %q from config is not present in plan", k))
   177  						continue
   178  					}
   179  				}
   180  			} else {
   181  				plannedL := plannedV.LengthInt()
   182  				configL := configV.LengthInt()
   183  				if plannedL != configL {
   184  					errs = append(errs, path.NewErrorf("block count in plan (%d) disagrees with count in config (%d)", plannedL, configL))
   185  					continue
   186  				}
   187  				for it := plannedV.ElementIterator(); it.Next(); {
   188  					idx, plannedEV := it.Element()
   189  					path := append(path, cty.IndexStep{Key: idx})
   190  					if !plannedEV.IsKnown() {
   191  						errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead"))
   192  						continue
   193  					}
   194  					k := idx.AsString()
   195  					if !configV.HasIndex(idx).True() {
   196  						errs = append(errs, path.NewErrorf("block key %q from plan is not present in config", k))
   197  						continue
   198  					}
   199  					configEV := configV.Index(idx)
   200  					priorEV := cty.NullVal(blockS.ImpliedType())
   201  					if !priorV.IsNull() && priorV.HasIndex(idx).True() {
   202  						priorEV = priorV.Index(idx)
   203  					}
   204  					moreErrs := assertPlanValid(&blockS.Block, priorEV, configEV, plannedEV, path)
   205  					errs = append(errs, moreErrs...)
   206  				}
   207  				for it := configV.ElementIterator(); it.Next(); {
   208  					idx, _ := it.Element()
   209  					if !plannedV.HasIndex(idx).True() {
   210  						errs = append(errs, path.NewErrorf("block key %q from config is not present in plan", idx.AsString()))
   211  						continue
   212  					}
   213  				}
   214  			}
   215  		case configschema.NestingSet:
   216  			if plannedV.IsNull() {
   217  				errs = append(errs, path.NewErrorf("attribute representing a set of nested blocks must be empty to indicate no blocks, not null"))
   218  				continue
   219  			}
   220  
   221  			// Because set elements have no identifier with which to correlate
   222  			// them, we can't robustly validate the plan for a nested block
   223  			// backed by a set, and so unfortunately we need to just trust the
   224  			// provider to do the right thing. :(
   225  			//
   226  			// (In principle we could correlate elements by matching the
   227  			// subset of attributes explicitly set in config, except for the
   228  			// special diff suppression rule which allows for there to be a
   229  			// planned value that is constructed by mixing part of a prior
   230  			// value with part of a config value, creating an entirely new
   231  			// element that is not present in either prior nor config.)
   232  			for it := plannedV.ElementIterator(); it.Next(); {
   233  				idx, plannedEV := it.Element()
   234  				path := append(path, cty.IndexStep{Key: idx})
   235  				if !plannedEV.IsKnown() {
   236  					errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead"))
   237  					continue
   238  				}
   239  			}
   240  
   241  		default:
   242  			panic(fmt.Sprintf("unsupported nesting mode %s", blockS.Nesting))
   243  		}
   244  	}
   245  
   246  	return errs
   247  }
   248  
   249  func assertPlannedAttrsValid(schema map[string]*configschema.Attribute, priorState, config, plannedState cty.Value, path cty.Path) []error {
   250  	var errs []error
   251  	for name, attrS := range schema {
   252  		moreErrs := assertPlannedAttrValid(name, attrS, priorState, config, plannedState, path)
   253  		errs = append(errs, moreErrs...)
   254  	}
   255  	return errs
   256  }
   257  
   258  func assertPlannedAttrValid(name string, attrS *configschema.Attribute, priorState, config, plannedState cty.Value, path cty.Path) []error {
   259  	plannedV := plannedState.GetAttr(name)
   260  	configV := config.GetAttr(name)
   261  	priorV := cty.NullVal(attrS.Type)
   262  	if !priorState.IsNull() {
   263  		priorV = priorState.GetAttr(name)
   264  	}
   265  	path = append(path, cty.GetAttrStep{Name: name})
   266  
   267  	return assertPlannedValueValid(attrS, priorV, configV, plannedV, path)
   268  }
   269  
   270  func assertPlannedValueValid(attrS *configschema.Attribute, priorV, configV, plannedV cty.Value, path cty.Path) []error {
   271  
   272  	var errs []error
   273  	if unrefinedValue(plannedV).RawEquals(unrefinedValue(configV)) {
   274  		// This is the easy path: provider didn't change anything at all.
   275  		return errs
   276  	}
   277  	if unrefinedValue(plannedV).RawEquals(unrefinedValue(priorV)) && !priorV.IsNull() && !configV.IsNull() {
   278  		// Also pretty easy: there is a prior value and the provider has
   279  		// returned it unchanged. This indicates that configV and plannedV
   280  		// are functionally equivalent and so the provider wishes to disregard
   281  		// the configuration value in favor of the prior.
   282  		return errs
   283  	}
   284  
   285  	switch {
   286  	// The provider can plan any value for a computed-only attribute. There may
   287  	// be a config value here in the case where a user used `ignore_changes` on
   288  	// a computed attribute and ignored the warning, or we failed to validate
   289  	// computed attributes in the config, but regardless it's not a plan error
   290  	// caused by the provider.
   291  	case attrS.Computed && !attrS.Optional:
   292  		return errs
   293  
   294  	// The provider is allowed to insert optional values when the config is
   295  	// null, but only if the attribute is computed.
   296  	case configV.IsNull() && attrS.Computed:
   297  		return errs
   298  
   299  	case configV.IsNull() && !plannedV.IsNull():
   300  		// if the attribute is not computed, then any planned value is incorrect
   301  		if attrS.Sensitive {
   302  			errs = append(errs, path.NewErrorf("sensitive planned value for a non-computed attribute"))
   303  		} else {
   304  			errs = append(errs, path.NewErrorf("planned value %#v for a non-computed attribute", plannedV))
   305  		}
   306  		return errs
   307  	}
   308  
   309  	// If this attribute has a NestedType, validate the nested object
   310  	if attrS.NestedType != nil {
   311  		return assertPlannedObjectValid(attrS.NestedType, priorV, configV, plannedV, path)
   312  	}
   313  
   314  	// If none of the above conditions match, the provider has made an invalid
   315  	// change to this attribute.
   316  	if priorV.IsNull() {
   317  		if attrS.Sensitive {
   318  			errs = append(errs, path.NewErrorf("sensitive planned value does not match config value"))
   319  		} else {
   320  			errs = append(errs, path.NewErrorf("planned value %#v does not match config value %#v", plannedV, configV))
   321  		}
   322  		return errs
   323  	}
   324  
   325  	if attrS.Sensitive {
   326  		errs = append(errs, path.NewErrorf("sensitive planned value does not match config value nor prior value"))
   327  	} else {
   328  		errs = append(errs, path.NewErrorf("planned value %#v does not match config value %#v nor prior value %#v", plannedV, configV, priorV))
   329  	}
   330  
   331  	return errs
   332  }
   333  
   334  func assertPlannedObjectValid(schema *configschema.Object, prior, config, planned cty.Value, path cty.Path) []error {
   335  	var errs []error
   336  
   337  	if planned.IsNull() && !config.IsNull() {
   338  		errs = append(errs, path.NewErrorf("planned for absence but config wants existence"))
   339  		return errs
   340  	}
   341  	if config.IsNull() && !planned.IsNull() {
   342  		errs = append(errs, path.NewErrorf("planned for existence but config wants absence"))
   343  		return errs
   344  	}
   345  	if !config.IsNull() && !planned.IsKnown() {
   346  		errs = append(errs, path.NewErrorf("planned unknown for configured value"))
   347  		return errs
   348  	}
   349  
   350  	if planned.IsNull() {
   351  		// No further checks possible if the planned value is null
   352  		return errs
   353  	}
   354  
   355  	switch schema.Nesting {
   356  	case configschema.NestingSingle, configschema.NestingGroup:
   357  		moreErrs := assertPlannedAttrsValid(schema.Attributes, prior, config, planned, path)
   358  		errs = append(errs, moreErrs...)
   359  
   360  	case configschema.NestingList:
   361  		// A NestingList might either be a list or a tuple, depending on
   362  		// whether there are dynamically-typed attributes inside. However,
   363  		// both support a similar-enough API that we can treat them the
   364  		// same for our purposes here.
   365  
   366  		plannedL := planned.Length()
   367  		configL := config.Length()
   368  
   369  		// config wasn't known, then planned should be unknown too
   370  		if !plannedL.IsKnown() && !configL.IsKnown() {
   371  			return errs
   372  		}
   373  
   374  		lenEqual := plannedL.Equals(configL)
   375  		if !lenEqual.IsKnown() || lenEqual.False() {
   376  			errs = append(errs, path.NewErrorf("count in plan (%#v) disagrees with count in config (%#v)", plannedL, configL))
   377  			return errs
   378  		}
   379  		for it := planned.ElementIterator(); it.Next(); {
   380  			idx, plannedEV := it.Element()
   381  			path := append(path, cty.IndexStep{Key: idx})
   382  			if !config.HasIndex(idx).True() {
   383  				continue // should never happen since we checked the lengths above
   384  			}
   385  			configEV := config.Index(idx)
   386  			priorEV := cty.NullVal(schema.ImpliedType())
   387  			if !prior.IsNull() && prior.HasIndex(idx).True() {
   388  				priorEV = prior.Index(idx)
   389  			}
   390  
   391  			moreErrs := assertPlannedAttrsValid(schema.Attributes, priorEV, configEV, plannedEV, path)
   392  			errs = append(errs, moreErrs...)
   393  		}
   394  
   395  	case configschema.NestingMap:
   396  		// A NestingMap might either be a map or an object, depending on
   397  		// whether there are dynamically-typed attributes inside, so we will
   398  		// break these down to maps to handle them both in the same manner.
   399  		plannedVals := map[string]cty.Value{}
   400  		configVals := map[string]cty.Value{}
   401  		priorVals := map[string]cty.Value{}
   402  
   403  		plannedL := planned.Length()
   404  		configL := config.Length()
   405  
   406  		// config wasn't known, then planned should be unknown too
   407  		if !plannedL.IsKnown() && !configL.IsKnown() {
   408  			return errs
   409  		}
   410  
   411  		lenEqual := plannedL.Equals(configL)
   412  		if !lenEqual.IsKnown() || lenEqual.False() {
   413  			errs = append(errs, path.NewErrorf("count in plan (%#v) disagrees with count in config (%#v)", plannedL, configL))
   414  			return errs
   415  		}
   416  
   417  		if !planned.IsNull() {
   418  			plannedVals = planned.AsValueMap()
   419  		}
   420  		if !config.IsNull() {
   421  			configVals = config.AsValueMap()
   422  		}
   423  		if !prior.IsNull() {
   424  			priorVals = prior.AsValueMap()
   425  		}
   426  
   427  		for k, plannedEV := range plannedVals {
   428  			configEV, ok := configVals[k]
   429  			if !ok {
   430  				errs = append(errs, path.NewErrorf("map key %q from plan is not present in config", k))
   431  				continue
   432  			}
   433  			path := append(path, cty.GetAttrStep{Name: k})
   434  
   435  			priorEV, ok := priorVals[k]
   436  			if !ok {
   437  				priorEV = cty.NullVal(schema.ImpliedType())
   438  			}
   439  			moreErrs := assertPlannedAttrsValid(schema.Attributes, priorEV, configEV, plannedEV, path)
   440  			errs = append(errs, moreErrs...)
   441  		}
   442  		for k := range configVals {
   443  			if _, ok := plannedVals[k]; !ok {
   444  				errs = append(errs, path.NewErrorf("map key %q from config is not present in plan", k))
   445  				continue
   446  			}
   447  		}
   448  
   449  	case configschema.NestingSet:
   450  		plannedL := planned.Length()
   451  		configL := config.Length()
   452  
   453  		if ok := plannedL.Range().Includes(configL); ok.IsKnown() && ok.False() {
   454  			errs = append(errs, path.NewErrorf("count in plan (%#v) disagrees with count in config (%#v)", plannedL, configL))
   455  			return errs
   456  		}
   457  		// Because set elements have no identifier with which to correlate
   458  		// them, we can't robustly validate the plan for a nested object
   459  		// backed by a set, and so unfortunately we need to just trust the
   460  		// provider to do the right thing.
   461  	}
   462  
   463  	return errs
   464  }
   465  
   466  // unrefinedValue returns the given value with any unknown value refinements
   467  // stripped away, making it a basic unknown value with only a type constraint.
   468  //
   469  // This function also considers unknown values nested inside a known container
   470  // such as a collection, which unfortunately makes it relatively expensive
   471  // for large data structures. Over time we should transition away from using
   472  // this trick and prefer to use cty's Equals and value range APIs instead of
   473  // of using Value.RawEquals, which is primarily intended for unit test code
   474  // rather than real application use.
   475  func unrefinedValue(v cty.Value) cty.Value {
   476  	ret, _ := cty.Transform(v, func(p cty.Path, v cty.Value) (cty.Value, error) {
   477  		if !v.IsKnown() {
   478  			return cty.UnknownVal(v.Type()), nil
   479  		}
   480  		return v, nil
   481  	})
   482  	return ret
   483  }