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