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 }