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 }