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