github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/plans/objchange/compatible.go (about) 1 package objchange 2 3 import ( 4 "fmt" 5 "strconv" 6 7 "github.com/zclconf/go-cty/cty" 8 "github.com/zclconf/go-cty/cty/convert" 9 10 "github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema" 11 ) 12 13 // AssertObjectCompatible checks whether the given "actual" value is a valid 14 // completion of the possibly-partially-unknown "planned" value. 15 // 16 // This means that any known leaf value in "planned" must be equal to the 17 // corresponding value in "actual", and various other similar constraints. 18 // 19 // Any inconsistencies are reported by returning a non-zero number of errors. 20 // These errors are usually (but not necessarily) cty.PathError values 21 // referring to a particular nested value within the "actual" value. 22 // 23 // The two values must have types that conform to the given schema's implied 24 // type, or this function will panic. 25 func AssertObjectCompatible(schema *configschema.Block, planned, actual cty.Value) []error { 26 return assertObjectCompatible(schema, planned, actual, nil) 27 } 28 29 func assertObjectCompatible(schema *configschema.Block, planned, actual cty.Value, path cty.Path) []error { 30 var errs []error 31 if planned.IsNull() && !actual.IsNull() { 32 errs = append(errs, path.NewErrorf("was absent, but now present")) 33 return errs 34 } 35 if actual.IsNull() && !planned.IsNull() { 36 errs = append(errs, path.NewErrorf("was present, but now absent")) 37 return errs 38 } 39 if planned.IsNull() { 40 // No further checks possible if both values are null 41 return errs 42 } 43 44 for name, attrS := range schema.Attributes { 45 plannedV := planned.GetAttr(name) 46 actualV := actual.GetAttr(name) 47 48 path := append(path, cty.GetAttrStep{Name: name}) 49 moreErrs := assertValueCompatible(plannedV, actualV, path) 50 if attrS.Sensitive { 51 if len(moreErrs) > 0 { 52 // Use a vague placeholder message instead, to avoid disclosing 53 // sensitive information. 54 errs = append(errs, path.NewErrorf("inconsistent values for sensitive attribute")) 55 } 56 } else { 57 errs = append(errs, moreErrs...) 58 } 59 } 60 for name, blockS := range schema.BlockTypes { 61 plannedV := planned.GetAttr(name) 62 actualV := actual.GetAttr(name) 63 64 // As a special case, if there were any blocks whose leaf attributes 65 // are all unknown then we assume (possibly incorrectly) that the 66 // HCL dynamic block extension is in use with an unknown for_each 67 // argument, and so we will do looser validation here that allows 68 // for those blocks to have expanded into a different number of blocks 69 // if the for_each value is now known. 70 maybeUnknownBlocks := couldHaveUnknownBlockPlaceholder(plannedV, blockS, false) 71 72 path := append(path, cty.GetAttrStep{Name: name}) 73 switch blockS.Nesting { 74 case configschema.NestingSingle, configschema.NestingGroup: 75 // If an unknown block placeholder was present then the placeholder 76 // may have expanded out into zero blocks, which is okay. 77 if maybeUnknownBlocks && actualV.IsNull() { 78 continue 79 } 80 moreErrs := assertObjectCompatible(&blockS.Block, plannedV, actualV, path) 81 errs = append(errs, moreErrs...) 82 case configschema.NestingList: 83 // A NestingList might either be a list or a tuple, depending on 84 // whether there are dynamically-typed attributes inside. However, 85 // both support a similar-enough API that we can treat them the 86 // same for our purposes here. 87 if !plannedV.IsKnown() || !actualV.IsKnown() || plannedV.IsNull() || actualV.IsNull() { 88 continue 89 } 90 91 if maybeUnknownBlocks { 92 // When unknown blocks are present the final blocks may be 93 // at different indices than the planned blocks, so unfortunately 94 // we can't do our usual checks in this case without generating 95 // false negatives. 96 continue 97 } 98 99 plannedL := plannedV.LengthInt() 100 actualL := actualV.LengthInt() 101 if plannedL != actualL { 102 errs = append(errs, path.NewErrorf("block count changed from %d to %d", plannedL, actualL)) 103 continue 104 } 105 for it := plannedV.ElementIterator(); it.Next(); { 106 idx, plannedEV := it.Element() 107 if !actualV.HasIndex(idx).True() { 108 continue 109 } 110 actualEV := actualV.Index(idx) 111 moreErrs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.IndexStep{Key: idx})) 112 errs = append(errs, moreErrs...) 113 } 114 case configschema.NestingMap: 115 // A NestingMap might either be a map or an object, depending on 116 // whether there are dynamically-typed attributes inside, but 117 // that's decided statically and so both values will have the same 118 // kind. 119 if plannedV.Type().IsObjectType() { 120 plannedAtys := plannedV.Type().AttributeTypes() 121 actualAtys := actualV.Type().AttributeTypes() 122 for k := range plannedAtys { 123 if _, ok := actualAtys[k]; !ok { 124 errs = append(errs, path.NewErrorf("block key %q has vanished", k)) 125 continue 126 } 127 128 plannedEV := plannedV.GetAttr(k) 129 actualEV := actualV.GetAttr(k) 130 moreErrs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.GetAttrStep{Name: k})) 131 errs = append(errs, moreErrs...) 132 } 133 if !maybeUnknownBlocks { // new blocks may appear if unknown blocks were present in the plan 134 for k := range actualAtys { 135 if _, ok := plannedAtys[k]; !ok { 136 errs = append(errs, path.NewErrorf("new block key %q has appeared", k)) 137 continue 138 } 139 } 140 } 141 } else { 142 if !plannedV.IsKnown() || plannedV.IsNull() || actualV.IsNull() { 143 continue 144 } 145 plannedL := plannedV.LengthInt() 146 actualL := actualV.LengthInt() 147 if plannedL != actualL && !maybeUnknownBlocks { // new blocks may appear if unknown blocks were persent in the plan 148 errs = append(errs, path.NewErrorf("block count changed from %d to %d", plannedL, actualL)) 149 continue 150 } 151 for it := plannedV.ElementIterator(); it.Next(); { 152 idx, plannedEV := it.Element() 153 if !actualV.HasIndex(idx).True() { 154 continue 155 } 156 actualEV := actualV.Index(idx) 157 moreErrs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.IndexStep{Key: idx})) 158 errs = append(errs, moreErrs...) 159 } 160 } 161 case configschema.NestingSet: 162 if !plannedV.IsKnown() || !actualV.IsKnown() || plannedV.IsNull() || actualV.IsNull() { 163 continue 164 } 165 166 setErrs := assertSetValuesCompatible(plannedV, actualV, path, func(plannedEV, actualEV cty.Value) bool { 167 errs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.IndexStep{Key: actualEV})) 168 return len(errs) == 0 169 }) 170 errs = append(errs, setErrs...) 171 172 if maybeUnknownBlocks { 173 // When unknown blocks are present the final number of blocks 174 // may be different, either because the unknown set values 175 // become equal and are collapsed, or the count is unknown due 176 // a dynamic block. Unfortunately this means we can't do our 177 // usual checks in this case without generating false 178 // negatives. 179 continue 180 } 181 182 // There can be fewer elements in a set after its elements are all 183 // known (values that turn out to be equal will coalesce) but the 184 // number of elements must never get larger. 185 plannedL := plannedV.LengthInt() 186 actualL := actualV.LengthInt() 187 if plannedL < actualL { 188 errs = append(errs, path.NewErrorf("block set length changed from %d to %d", plannedL, actualL)) 189 } 190 default: 191 panic(fmt.Sprintf("unsupported nesting mode %s", blockS.Nesting)) 192 } 193 } 194 return errs 195 } 196 197 func assertValueCompatible(planned, actual cty.Value, path cty.Path) []error { 198 // NOTE: We don't normally use the GoString rendering of cty.Value in 199 // user-facing error messages as a rule, but we make an exception 200 // for this function because we expect the user to pass this message on 201 // verbatim to the provider development team and so more detail is better. 202 203 var errs []error 204 if planned.Type() == cty.DynamicPseudoType { 205 // Anything goes, then 206 return errs 207 } 208 if problems := planned.Type().TestConformance(actual.Type()); len(problems) > 0 { 209 errs = append(errs, path.NewErrorf("wrong final value type: %s", convert.MismatchMessage(actual.Type(), planned.Type()))) 210 // If the types don't match then we can't do any other comparisons, 211 // so we bail early. 212 return errs 213 } 214 215 if !planned.IsKnown() { 216 // We didn't know what were going to end up with during plan, so 217 // anything goes during apply. 218 return errs 219 } 220 221 if actual.IsNull() { 222 if planned.IsNull() { 223 return nil 224 } 225 errs = append(errs, path.NewErrorf("was %#v, but now null", planned)) 226 return errs 227 } 228 if planned.IsNull() { 229 errs = append(errs, path.NewErrorf("was null, but now %#v", actual)) 230 return errs 231 } 232 233 ty := planned.Type() 234 switch { 235 236 case !actual.IsKnown(): 237 errs = append(errs, path.NewErrorf("was known, but now unknown")) 238 239 case ty.IsPrimitiveType(): 240 if !actual.Equals(planned).True() { 241 errs = append(errs, path.NewErrorf("was %#v, but now %#v", planned, actual)) 242 } 243 244 case ty.IsListType() || ty.IsMapType() || ty.IsTupleType(): 245 for it := planned.ElementIterator(); it.Next(); { 246 k, plannedV := it.Element() 247 if !actual.HasIndex(k).True() { 248 errs = append(errs, path.NewErrorf("element %s has vanished", indexStrForErrors(k))) 249 continue 250 } 251 252 actualV := actual.Index(k) 253 moreErrs := assertValueCompatible(plannedV, actualV, append(path, cty.IndexStep{Key: k})) 254 errs = append(errs, moreErrs...) 255 } 256 257 for it := actual.ElementIterator(); it.Next(); { 258 k, _ := it.Element() 259 if !planned.HasIndex(k).True() { 260 errs = append(errs, path.NewErrorf("new element %s has appeared", indexStrForErrors(k))) 261 } 262 } 263 264 case ty.IsObjectType(): 265 atys := ty.AttributeTypes() 266 for name := range atys { 267 // Because we already tested that the two values have the same type, 268 // we can assume that the same attributes are present in both and 269 // focus just on testing their values. 270 plannedV := planned.GetAttr(name) 271 actualV := actual.GetAttr(name) 272 moreErrs := assertValueCompatible(plannedV, actualV, append(path, cty.GetAttrStep{Name: name})) 273 errs = append(errs, moreErrs...) 274 } 275 276 case ty.IsSetType(): 277 // We can't really do anything useful for sets here because changing 278 // an unknown element to known changes the identity of the element, and 279 // so we can't correlate them properly. However, we will at least check 280 // to ensure that the number of elements is consistent, along with 281 // the general type-match checks we ran earlier in this function. 282 if planned.IsKnown() && !planned.IsNull() && !actual.IsNull() { 283 284 setErrs := assertSetValuesCompatible(planned, actual, path, func(plannedV, actualV cty.Value) bool { 285 errs := assertValueCompatible(plannedV, actualV, append(path, cty.IndexStep{Key: actualV})) 286 return len(errs) == 0 287 }) 288 errs = append(errs, setErrs...) 289 290 // There can be fewer elements in a set after its elements are all 291 // known (values that turn out to be equal will coalesce) but the 292 // number of elements must never get larger. 293 294 plannedL := planned.LengthInt() 295 actualL := actual.LengthInt() 296 if plannedL < actualL { 297 errs = append(errs, path.NewErrorf("length changed from %d to %d", plannedL, actualL)) 298 } 299 } 300 } 301 302 return errs 303 } 304 305 func indexStrForErrors(v cty.Value) string { 306 switch v.Type() { 307 case cty.Number: 308 return v.AsBigFloat().Text('f', -1) 309 case cty.String: 310 return strconv.Quote(v.AsString()) 311 default: 312 // Should be impossible, since no other index types are allowed! 313 return fmt.Sprintf("%#v", v) 314 } 315 } 316 317 // couldHaveUnknownBlockPlaceholder is a heuristic that recognizes how the 318 // HCL dynamic block extension behaves when it's asked to expand a block whose 319 // for_each argument is unknown. In such cases, it generates a single placeholder 320 // block with all leaf attribute values unknown, and once the for_each 321 // expression becomes known the placeholder may be replaced with any number 322 // of blocks, so object compatibility checks would need to be more liberal. 323 // 324 // Set "nested" if testing a block that is nested inside a candidate block 325 // placeholder; this changes the interpretation of there being no blocks of 326 // a type to allow for there being zero nested blocks. 327 func couldHaveUnknownBlockPlaceholder(v cty.Value, blockS *configschema.NestedBlock, nested bool) bool { 328 switch blockS.Nesting { 329 case configschema.NestingSingle, configschema.NestingGroup: 330 if nested && v.IsNull() { 331 return true // for nested blocks, a single block being unset doesn't disqualify from being an unknown block placeholder 332 } 333 return couldBeUnknownBlockPlaceholderElement(v, &blockS.Block) 334 default: 335 // These situations should be impossible for correct providers, but 336 // we permit the legacy SDK to produce some incorrect outcomes 337 // for compatibility with its existing logic, and so we must be 338 // tolerant here. 339 if !v.IsKnown() { 340 return true 341 } 342 if v.IsNull() { 343 return false // treated as if the list were empty, so we would see zero iterations below 344 } 345 346 // For all other nesting modes, our value should be something iterable. 347 for it := v.ElementIterator(); it.Next(); { 348 _, ev := it.Element() 349 if couldBeUnknownBlockPlaceholderElement(ev, &blockS.Block) { 350 return true 351 } 352 } 353 354 // Our default changes depending on whether we're testing the candidate 355 // block itself or something nested inside of it: zero blocks of a type 356 // can never contain a dynamic block placeholder, but a dynamic block 357 // placeholder might contain zero blocks of one of its own nested block 358 // types, if none were set in the config at all. 359 return nested 360 } 361 } 362 363 func couldBeUnknownBlockPlaceholderElement(v cty.Value, schema *configschema.Block) bool { 364 if v.IsNull() { 365 return false // null value can never be a placeholder element 366 } 367 if !v.IsKnown() { 368 return true // this should never happen for well-behaved providers, but can happen with the legacy SDK opt-outs 369 } 370 for name := range schema.Attributes { 371 av := v.GetAttr(name) 372 373 // Unknown block placeholders contain only unknown or null attribute 374 // values, depending on whether or not a particular attribute was set 375 // explicitly inside the content block. Note that this is imprecise: 376 // non-placeholders can also match this, so this function can generate 377 // false positives. 378 if av.IsKnown() && !av.IsNull() { 379 return false 380 } 381 } 382 for name, blockS := range schema.BlockTypes { 383 if !couldHaveUnknownBlockPlaceholder(v.GetAttr(name), blockS, true) { 384 return false 385 } 386 } 387 return true 388 } 389 390 // assertSetValuesCompatible checks that each of the elements in a can 391 // be correlated with at least one equivalent element in b and vice-versa, 392 // using the given correlation function. 393 // 394 // This allows the number of elements in the sets to change as long as all 395 // elements in both sets can be correlated, making this function safe to use 396 // with sets that may contain unknown values as long as the unknown case is 397 // addressed in some reasonable way in the callback function. 398 // 399 // The callback always recieves values from set a as its first argument and 400 // values from set b in its second argument, so it is safe to use with 401 // non-commutative functions. 402 // 403 // As with assertValueCompatible, we assume that the target audience of error 404 // messages here is a provider developer (via a bug report from a user) and so 405 // we intentionally violate our usual rule of keeping cty implementation 406 // details out of error messages. 407 func assertSetValuesCompatible(planned, actual cty.Value, path cty.Path, f func(aVal, bVal cty.Value) bool) []error { 408 a := planned 409 b := actual 410 411 // Our methodology here is a little tricky, to deal with the fact that 412 // it's impossible to directly correlate two non-equal set elements because 413 // they don't have identities separate from their values. 414 // The approach is to count the number of equivalent elements each element 415 // of a has in b and vice-versa, and then return true only if each element 416 // in both sets has at least one equivalent. 417 as := a.AsValueSlice() 418 bs := b.AsValueSlice() 419 aeqs := make([]bool, len(as)) 420 beqs := make([]bool, len(bs)) 421 for ai, av := range as { 422 for bi, bv := range bs { 423 if f(av, bv) { 424 aeqs[ai] = true 425 beqs[bi] = true 426 } 427 } 428 } 429 430 var errs []error 431 for i, eq := range aeqs { 432 if !eq { 433 errs = append(errs, path.NewErrorf("planned set element %#v does not correlate with any element in actual", as[i])) 434 } 435 } 436 if len(errs) > 0 { 437 // Exit early since otherwise we're likely to generate duplicate 438 // error messages from the other perspective in the subsequent loop. 439 return errs 440 } 441 for i, eq := range beqs { 442 if !eq { 443 errs = append(errs, path.NewErrorf("actual set element %#v does not correlate with any element in plan", bs[i])) 444 } 445 } 446 return errs 447 }