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