github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/plans/objchange/plan_valid.go (about)

     1  package objchange
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/zclconf/go-cty/cty"
     7  
     8  	"github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema"
     9  )
    10  
    11  // AssertPlanValid checks checks whether a planned new state returned by a
    12  // provider's PlanResourceChange method is suitable to achieve a change
    13  // from priorState to config. It returns a slice with nonzero length if
    14  // any problems are detected. Because problems here indicate bugs in the
    15  // provider that generated the plannedState, they are written with provider
    16  // developers as an audience, rather than end-users.
    17  //
    18  // All of the given values must have the same type and must conform to the
    19  // implied type of the given schema, or this function may panic or produce
    20  // garbage results.
    21  //
    22  // During planning, a provider may only make changes to attributes that are
    23  // null (unset) in the configuration and are marked as "computed" in the
    24  // resource type schema, in order to insert any default values the provider
    25  // may know about. If the default value cannot be determined until apply time,
    26  // the provider can return an unknown value. Providers are forbidden from
    27  // planning a change that disagrees with any non-null argument in the
    28  // configuration.
    29  //
    30  // As a special exception, providers _are_ allowed to provide attribute values
    31  // conflicting with configuration if and only if the planned value exactly
    32  // matches the corresponding attribute value in the prior state. The provider
    33  // can use this to signal that the new value is functionally equivalent to
    34  // the old and thus no change is required.
    35  func AssertPlanValid(schema *configschema.Block, priorState, config, plannedState cty.Value) []error {
    36  	return assertPlanValid(schema, priorState, config, plannedState, nil)
    37  }
    38  
    39  func assertPlanValid(schema *configschema.Block, priorState, config, plannedState cty.Value, path cty.Path) []error {
    40  	var errs []error
    41  	if plannedState.IsNull() && !config.IsNull() {
    42  		errs = append(errs, path.NewErrorf("planned for absense but config wants existence"))
    43  		return errs
    44  	}
    45  	if config.IsNull() && !plannedState.IsNull() {
    46  		errs = append(errs, path.NewErrorf("planned for existence but config wants absense"))
    47  		return errs
    48  	}
    49  	if plannedState.IsNull() {
    50  		// No further checks possible if the planned value is null
    51  		return errs
    52  	}
    53  
    54  	impTy := schema.ImpliedType()
    55  
    56  	for name, attrS := range schema.Attributes {
    57  		plannedV := plannedState.GetAttr(name)
    58  		configV := config.GetAttr(name)
    59  		priorV := cty.NullVal(attrS.Type)
    60  		if !priorState.IsNull() {
    61  			priorV = priorState.GetAttr(name)
    62  		}
    63  
    64  		path := append(path, cty.GetAttrStep{Name: name})
    65  		moreErrs := assertPlannedValueValid(attrS, priorV, configV, plannedV, path)
    66  		errs = append(errs, moreErrs...)
    67  	}
    68  	for name, blockS := range schema.BlockTypes {
    69  		path := append(path, cty.GetAttrStep{Name: name})
    70  		plannedV := plannedState.GetAttr(name)
    71  		configV := config.GetAttr(name)
    72  		priorV := cty.NullVal(impTy.AttributeType(name))
    73  		if !priorState.IsNull() {
    74  			priorV = priorState.GetAttr(name)
    75  		}
    76  		if plannedV.RawEquals(configV) {
    77  			// Easy path: nothing has changed at all
    78  			continue
    79  		}
    80  		if !plannedV.IsKnown() {
    81  			errs = append(errs, path.NewErrorf("attribute representing nested block must not be unknown itself; set nested attribute values to unknown instead"))
    82  			continue
    83  		}
    84  
    85  		switch blockS.Nesting {
    86  		case configschema.NestingSingle, configschema.NestingGroup:
    87  			moreErrs := assertPlanValid(&blockS.Block, priorV, configV, plannedV, path)
    88  			errs = append(errs, moreErrs...)
    89  		case configschema.NestingList:
    90  			// A NestingList might either be a list or a tuple, depending on
    91  			// whether there are dynamically-typed attributes inside. However,
    92  			// both support a similar-enough API that we can treat them the
    93  			// same for our purposes here.
    94  			if plannedV.IsNull() {
    95  				errs = append(errs, path.NewErrorf("attribute representing a list of nested blocks must be empty to indicate no blocks, not null"))
    96  				continue
    97  			}
    98  
    99  			plannedL := plannedV.LengthInt()
   100  			configL := configV.LengthInt()
   101  			if plannedL != configL {
   102  				errs = append(errs, path.NewErrorf("block count in plan (%d) disagrees with count in config (%d)", plannedL, configL))
   103  				continue
   104  			}
   105  			for it := plannedV.ElementIterator(); it.Next(); {
   106  				idx, plannedEV := it.Element()
   107  				path := append(path, cty.IndexStep{Key: idx})
   108  				if !plannedEV.IsKnown() {
   109  					errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead"))
   110  					continue
   111  				}
   112  				if !configV.HasIndex(idx).True() {
   113  					continue // should never happen since we checked the lengths above
   114  				}
   115  				configEV := configV.Index(idx)
   116  				priorEV := cty.NullVal(blockS.ImpliedType())
   117  				if !priorV.IsNull() && priorV.HasIndex(idx).True() {
   118  					priorEV = priorV.Index(idx)
   119  				}
   120  
   121  				moreErrs := assertPlanValid(&blockS.Block, priorEV, configEV, plannedEV, path)
   122  				errs = append(errs, moreErrs...)
   123  			}
   124  		case configschema.NestingMap:
   125  			if plannedV.IsNull() {
   126  				errs = append(errs, path.NewErrorf("attribute representing a map of nested blocks must be empty to indicate no blocks, not null"))
   127  				continue
   128  			}
   129  
   130  			// A NestingMap might either be a map or an object, depending on
   131  			// whether there are dynamically-typed attributes inside, but
   132  			// that's decided statically and so all values will have the same
   133  			// kind.
   134  			if plannedV.Type().IsObjectType() {
   135  				plannedAtys := plannedV.Type().AttributeTypes()
   136  				configAtys := configV.Type().AttributeTypes()
   137  				for k := range plannedAtys {
   138  					if _, ok := configAtys[k]; !ok {
   139  						errs = append(errs, path.NewErrorf("block key %q from plan is not present in config", k))
   140  						continue
   141  					}
   142  					path := append(path, cty.GetAttrStep{Name: k})
   143  
   144  					plannedEV := plannedV.GetAttr(k)
   145  					if !plannedEV.IsKnown() {
   146  						errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead"))
   147  						continue
   148  					}
   149  					configEV := configV.GetAttr(k)
   150  					priorEV := cty.NullVal(blockS.ImpliedType())
   151  					if !priorV.IsNull() && priorV.Type().HasAttribute(k) {
   152  						priorEV = priorV.GetAttr(k)
   153  					}
   154  					moreErrs := assertPlanValid(&blockS.Block, priorEV, configEV, plannedEV, path)
   155  					errs = append(errs, moreErrs...)
   156  				}
   157  				for k := range configAtys {
   158  					if _, ok := plannedAtys[k]; !ok {
   159  						errs = append(errs, path.NewErrorf("block key %q from config is not present in plan", k))
   160  						continue
   161  					}
   162  				}
   163  			} else {
   164  				plannedL := plannedV.LengthInt()
   165  				configL := configV.LengthInt()
   166  				if plannedL != configL {
   167  					errs = append(errs, path.NewErrorf("block count in plan (%d) disagrees with count in config (%d)", plannedL, configL))
   168  					continue
   169  				}
   170  				for it := plannedV.ElementIterator(); it.Next(); {
   171  					idx, plannedEV := it.Element()
   172  					path := append(path, cty.IndexStep{Key: idx})
   173  					if !plannedEV.IsKnown() {
   174  						errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead"))
   175  						continue
   176  					}
   177  					k := idx.AsString()
   178  					if !configV.HasIndex(idx).True() {
   179  						errs = append(errs, path.NewErrorf("block key %q from plan is not present in config", k))
   180  						continue
   181  					}
   182  					configEV := configV.Index(idx)
   183  					priorEV := cty.NullVal(blockS.ImpliedType())
   184  					if !priorV.IsNull() && priorV.HasIndex(idx).True() {
   185  						priorEV = priorV.Index(idx)
   186  					}
   187  					moreErrs := assertPlanValid(&blockS.Block, priorEV, configEV, plannedEV, path)
   188  					errs = append(errs, moreErrs...)
   189  				}
   190  				for it := configV.ElementIterator(); it.Next(); {
   191  					idx, _ := it.Element()
   192  					if !plannedV.HasIndex(idx).True() {
   193  						errs = append(errs, path.NewErrorf("block key %q from config is not present in plan", idx.AsString()))
   194  						continue
   195  					}
   196  				}
   197  			}
   198  		case configschema.NestingSet:
   199  			if plannedV.IsNull() {
   200  				errs = append(errs, path.NewErrorf("attribute representing a set of nested blocks must be empty to indicate no blocks, not null"))
   201  				continue
   202  			}
   203  
   204  			// Because set elements have no identifier with which to correlate
   205  			// them, we can't robustly validate the plan for a nested block
   206  			// backed by a set, and so unfortunately we need to just trust the
   207  			// provider to do the right thing. :(
   208  			//
   209  			// (In principle we could correlate elements by matching the
   210  			// subset of attributes explicitly set in config, except for the
   211  			// special diff suppression rule which allows for there to be a
   212  			// planned value that is constructed by mixing part of a prior
   213  			// value with part of a config value, creating an entirely new
   214  			// element that is not present in either prior nor config.)
   215  			for it := plannedV.ElementIterator(); it.Next(); {
   216  				idx, plannedEV := it.Element()
   217  				path := append(path, cty.IndexStep{Key: idx})
   218  				if !plannedEV.IsKnown() {
   219  					errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead"))
   220  					continue
   221  				}
   222  			}
   223  
   224  		default:
   225  			panic(fmt.Sprintf("unsupported nesting mode %s", blockS.Nesting))
   226  		}
   227  	}
   228  
   229  	return errs
   230  }
   231  
   232  func assertPlannedValueValid(attrS *configschema.Attribute, priorV, configV, plannedV cty.Value, path cty.Path) []error {
   233  	var errs []error
   234  	if plannedV.RawEquals(configV) {
   235  		// This is the easy path: provider didn't change anything at all.
   236  		return errs
   237  	}
   238  	if plannedV.RawEquals(priorV) && !priorV.IsNull() {
   239  		// Also pretty easy: there is a prior value and the provider has
   240  		// returned it unchanged. This indicates that configV and plannedV
   241  		// are functionally equivalent and so the provider wishes to disregard
   242  		// the configuration value in favor of the prior.
   243  		return errs
   244  	}
   245  	if attrS.Computed && configV.IsNull() {
   246  		// The provider is allowed to change the value of any computed
   247  		// attribute that isn't explicitly set in the config.
   248  		return errs
   249  	}
   250  
   251  	// If none of the above conditions match, the provider has made an invalid
   252  	// change to this attribute.
   253  	if priorV.IsNull() {
   254  		if attrS.Sensitive {
   255  			errs = append(errs, path.NewErrorf("sensitive planned value does not match config value"))
   256  		} else {
   257  			errs = append(errs, path.NewErrorf("planned value %#v does not match config value %#v", plannedV, configV))
   258  		}
   259  		return errs
   260  	}
   261  	if attrS.Sensitive {
   262  		errs = append(errs, path.NewErrorf("sensitive planned value does not match config value nor prior value"))
   263  	} else {
   264  		errs = append(errs, path.NewErrorf("planned value %#v does not match config value %#v nor prior value %#v", plannedV, configV, priorV))
   265  	}
   266  	return errs
   267  }