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