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