kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/plans/objchange/plan_valid.go (about) 1 package objchange 2 3 import ( 4 "fmt" 5 6 "github.com/zclconf/go-cty/cty" 7 8 "kubeform.dev/terraform-backend-sdk/configs/configschema" 9 ) 10 11 // AssertPlanValid checks checks whether a planned new state returned by a 12 // provider's PlanResourceChange method is suitable to achieve a change 13 // from priorState to config. It returns a slice with nonzero length if 14 // any problems are detected. Because problems here indicate bugs in the 15 // provider that generated the plannedState, they are written with provider 16 // developers as an audience, rather than end-users. 17 // 18 // All of the given values must have the same type and must conform to the 19 // implied type of the given schema, or this function may panic or produce 20 // garbage results. 21 // 22 // During planning, a provider may only make changes to attributes that are 23 // null (unset) in the configuration and are marked as "computed" in the 24 // resource type schema, in order to insert any default values the provider 25 // may know about. If the default value cannot be determined until apply time, 26 // the provider can return an unknown value. Providers are forbidden from 27 // planning a change that disagrees with any non-null argument in the 28 // configuration. 29 // 30 // As a special exception, providers _are_ allowed to provide attribute values 31 // conflicting with configuration if and only if the planned value exactly 32 // matches the corresponding attribute value in the prior state. The provider 33 // can use this to signal that the new value is functionally equivalent to 34 // the old and thus no change is required. 35 func AssertPlanValid(schema *configschema.Block, priorState, config, plannedState cty.Value) []error { 36 return assertPlanValid(schema, priorState, config, plannedState, nil) 37 } 38 39 func assertPlanValid(schema *configschema.Block, priorState, config, plannedState cty.Value, path cty.Path) []error { 40 var errs []error 41 if plannedState.IsNull() && !config.IsNull() { 42 errs = append(errs, path.NewErrorf("planned for absence but config wants existence")) 43 return errs 44 } 45 if config.IsNull() && !plannedState.IsNull() { 46 errs = append(errs, path.NewErrorf("planned for existence but config wants absence")) 47 return errs 48 } 49 if plannedState.IsNull() { 50 // No further checks possible if the planned value is null 51 return errs 52 } 53 54 impTy := schema.ImpliedType() 55 56 // verify attributes 57 moreErrs := assertPlannedAttrsValid(schema.Attributes, priorState, config, plannedState, path) 58 errs = append(errs, moreErrs...) 59 60 for name, blockS := range schema.BlockTypes { 61 path := append(path, cty.GetAttrStep{Name: name}) 62 plannedV := plannedState.GetAttr(name) 63 configV := config.GetAttr(name) 64 priorV := cty.NullVal(impTy.AttributeType(name)) 65 if !priorState.IsNull() { 66 priorV = priorState.GetAttr(name) 67 } 68 if plannedV.RawEquals(configV) { 69 // Easy path: nothing has changed at all 70 continue 71 } 72 73 if !configV.IsKnown() { 74 // An unknown config block represents a dynamic block where the 75 // for_each value is unknown, and therefor cannot be altered by the 76 // provider. 77 errs = append(errs, path.NewErrorf("planned value %#v for unknown dynamic block", plannedV)) 78 continue 79 } 80 81 if !plannedV.IsKnown() { 82 // Only dynamic configuration can set blocks to unknown, so this is 83 // not allowed from the provider. This means that either the config 84 // and plan should match, or we have an error where the plan 85 // changed the config value, both of which have been checked. 86 errs = append(errs, path.NewErrorf("attribute representing nested block must not be unknown itself; set nested attribute values to unknown instead")) 87 continue 88 } 89 90 switch blockS.Nesting { 91 case configschema.NestingSingle, configschema.NestingGroup: 92 moreErrs := assertPlanValid(&blockS.Block, priorV, configV, plannedV, 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.IsNull() { 100 errs = append(errs, path.NewErrorf("attribute representing a list of nested blocks must be empty to indicate no blocks, not null")) 101 continue 102 } 103 104 plannedL := plannedV.LengthInt() 105 configL := configV.LengthInt() 106 if plannedL != configL { 107 errs = append(errs, path.NewErrorf("block count in plan (%d) disagrees with count in config (%d)", plannedL, configL)) 108 continue 109 } 110 111 for it := plannedV.ElementIterator(); it.Next(); { 112 idx, plannedEV := it.Element() 113 path := append(path, cty.IndexStep{Key: idx}) 114 if !plannedEV.IsKnown() { 115 errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead")) 116 continue 117 } 118 if !configV.HasIndex(idx).True() { 119 continue // should never happen since we checked the lengths above 120 } 121 configEV := configV.Index(idx) 122 priorEV := cty.NullVal(blockS.ImpliedType()) 123 if !priorV.IsNull() && priorV.HasIndex(idx).True() { 124 priorEV = priorV.Index(idx) 125 } 126 127 moreErrs := assertPlanValid(&blockS.Block, priorEV, configEV, plannedEV, path) 128 errs = append(errs, moreErrs...) 129 } 130 case configschema.NestingMap: 131 if plannedV.IsNull() { 132 errs = append(errs, path.NewErrorf("attribute representing a map of nested blocks must be empty to indicate no blocks, not null")) 133 continue 134 } 135 136 // A NestingMap might either be a map or an object, depending on 137 // whether there are dynamically-typed attributes inside, but 138 // that's decided statically and so all values will have the same 139 // kind. 140 if plannedV.Type().IsObjectType() { 141 plannedAtys := plannedV.Type().AttributeTypes() 142 configAtys := configV.Type().AttributeTypes() 143 for k := range plannedAtys { 144 if _, ok := configAtys[k]; !ok { 145 errs = append(errs, path.NewErrorf("block key %q from plan is not present in config", k)) 146 continue 147 } 148 path := append(path, cty.GetAttrStep{Name: k}) 149 150 plannedEV := plannedV.GetAttr(k) 151 if !plannedEV.IsKnown() { 152 errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead")) 153 continue 154 } 155 configEV := configV.GetAttr(k) 156 priorEV := cty.NullVal(blockS.ImpliedType()) 157 if !priorV.IsNull() && priorV.Type().HasAttribute(k) { 158 priorEV = priorV.GetAttr(k) 159 } 160 moreErrs := assertPlanValid(&blockS.Block, priorEV, configEV, plannedEV, path) 161 errs = append(errs, moreErrs...) 162 } 163 for k := range configAtys { 164 if _, ok := plannedAtys[k]; !ok { 165 errs = append(errs, path.NewErrorf("block key %q from config is not present in plan", k)) 166 continue 167 } 168 } 169 } else { 170 plannedL := plannedV.LengthInt() 171 configL := configV.LengthInt() 172 if plannedL != configL { 173 errs = append(errs, path.NewErrorf("block count in plan (%d) disagrees with count in config (%d)", plannedL, configL)) 174 continue 175 } 176 for it := plannedV.ElementIterator(); it.Next(); { 177 idx, plannedEV := it.Element() 178 path := append(path, cty.IndexStep{Key: idx}) 179 if !plannedEV.IsKnown() { 180 errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead")) 181 continue 182 } 183 k := idx.AsString() 184 if !configV.HasIndex(idx).True() { 185 errs = append(errs, path.NewErrorf("block key %q from plan is not present in config", k)) 186 continue 187 } 188 configEV := configV.Index(idx) 189 priorEV := cty.NullVal(blockS.ImpliedType()) 190 if !priorV.IsNull() && priorV.HasIndex(idx).True() { 191 priorEV = priorV.Index(idx) 192 } 193 moreErrs := assertPlanValid(&blockS.Block, priorEV, configEV, plannedEV, path) 194 errs = append(errs, moreErrs...) 195 } 196 for it := configV.ElementIterator(); it.Next(); { 197 idx, _ := it.Element() 198 if !plannedV.HasIndex(idx).True() { 199 errs = append(errs, path.NewErrorf("block key %q from config is not present in plan", idx.AsString())) 200 continue 201 } 202 } 203 } 204 case configschema.NestingSet: 205 if plannedV.IsNull() { 206 errs = append(errs, path.NewErrorf("attribute representing a set of nested blocks must be empty to indicate no blocks, not null")) 207 continue 208 } 209 210 // Because set elements have no identifier with which to correlate 211 // them, we can't robustly validate the plan for a nested block 212 // backed by a set, and so unfortunately we need to just trust the 213 // provider to do the right thing. :( 214 // 215 // (In principle we could correlate elements by matching the 216 // subset of attributes explicitly set in config, except for the 217 // special diff suppression rule which allows for there to be a 218 // planned value that is constructed by mixing part of a prior 219 // value with part of a config value, creating an entirely new 220 // element that is not present in either prior nor config.) 221 for it := plannedV.ElementIterator(); it.Next(); { 222 idx, plannedEV := it.Element() 223 path := append(path, cty.IndexStep{Key: idx}) 224 if !plannedEV.IsKnown() { 225 errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead")) 226 continue 227 } 228 } 229 230 default: 231 panic(fmt.Sprintf("unsupported nesting mode %s", blockS.Nesting)) 232 } 233 } 234 235 return errs 236 } 237 238 func assertPlannedAttrsValid(schema map[string]*configschema.Attribute, priorState, config, plannedState cty.Value, path cty.Path) []error { 239 var errs []error 240 for name, attrS := range schema { 241 moreErrs := assertPlannedAttrValid(name, attrS, priorState, config, plannedState, path) 242 errs = append(errs, moreErrs...) 243 } 244 return errs 245 } 246 247 func assertPlannedAttrValid(name string, attrS *configschema.Attribute, priorState, config, plannedState cty.Value, path cty.Path) []error { 248 plannedV := plannedState.GetAttr(name) 249 configV := config.GetAttr(name) 250 priorV := cty.NullVal(attrS.Type) 251 if !priorState.IsNull() { 252 priorV = priorState.GetAttr(name) 253 } 254 path = append(path, cty.GetAttrStep{Name: name}) 255 256 return assertPlannedValueValid(attrS, priorV, configV, plannedV, path) 257 } 258 259 func assertPlannedValueValid(attrS *configschema.Attribute, priorV, configV, plannedV cty.Value, path cty.Path) []error { 260 var errs []error 261 if plannedV.RawEquals(configV) { 262 // This is the easy path: provider didn't change anything at all. 263 return errs 264 } 265 if plannedV.RawEquals(priorV) && !priorV.IsNull() && !configV.IsNull() { 266 // Also pretty easy: there is a prior value and the provider has 267 // returned it unchanged. This indicates that configV and plannedV 268 // are functionally equivalent and so the provider wishes to disregard 269 // the configuration value in favor of the prior. 270 return errs 271 } 272 273 // the provider is allowed to insert values when the config is 274 // null, but only if the attribute is computed. 275 if configV.IsNull() { 276 if attrS.Computed { 277 return errs 278 } 279 280 // if the attribute is not computed, then any planned value is incorrect 281 if !plannedV.IsNull() { 282 if attrS.Sensitive { 283 errs = append(errs, path.NewErrorf("sensitive planned value for a non-computed attribute")) 284 } else { 285 errs = append(errs, path.NewErrorf("planned value %#v for a non-computed attribute", plannedV)) 286 } 287 return errs 288 } 289 } 290 291 // If this attribute has a NestedType, validate the nested object 292 if attrS.NestedType != nil { 293 return assertPlannedObjectValid(attrS.NestedType, priorV, configV, plannedV, path) 294 } 295 296 // If none of the above conditions match, the provider has made an invalid 297 // change to this attribute. 298 if priorV.IsNull() { 299 if attrS.Sensitive { 300 errs = append(errs, path.NewErrorf("sensitive planned value does not match config value")) 301 } else { 302 errs = append(errs, path.NewErrorf("planned value %#v does not match config value %#v", plannedV, configV)) 303 } 304 return errs 305 } 306 307 if attrS.Sensitive { 308 errs = append(errs, path.NewErrorf("sensitive planned value does not match config value nor prior value")) 309 } else { 310 errs = append(errs, path.NewErrorf("planned value %#v does not match config value %#v nor prior value %#v", plannedV, configV, priorV)) 311 } 312 313 return errs 314 } 315 316 func assertPlannedObjectValid(schema *configschema.Object, prior, config, planned cty.Value, path cty.Path) []error { 317 var errs []error 318 319 if planned.IsNull() && !config.IsNull() { 320 errs = append(errs, path.NewErrorf("planned for absence but config wants existence")) 321 return errs 322 } 323 if config.IsNull() && !planned.IsNull() { 324 errs = append(errs, path.NewErrorf("planned for existence but config wants absence")) 325 return errs 326 } 327 if planned.IsNull() { 328 // No further checks possible if the planned value is null 329 return errs 330 } 331 332 switch schema.Nesting { 333 case configschema.NestingSingle, configschema.NestingGroup: 334 moreErrs := assertPlannedAttrsValid(schema.Attributes, prior, config, planned, path) 335 errs = append(errs, moreErrs...) 336 337 case configschema.NestingList: 338 // A NestingList might either be a list or a tuple, depending on 339 // whether there are dynamically-typed attributes inside. However, 340 // both support a similar-enough API that we can treat them the 341 // same for our purposes here. 342 343 plannedL := planned.LengthInt() 344 configL := config.LengthInt() 345 if plannedL != configL { 346 errs = append(errs, path.NewErrorf("count in plan (%d) disagrees with count in config (%d)", plannedL, configL)) 347 return errs 348 } 349 for it := planned.ElementIterator(); it.Next(); { 350 idx, plannedEV := it.Element() 351 path := append(path, cty.IndexStep{Key: idx}) 352 if !config.HasIndex(idx).True() { 353 continue // should never happen since we checked the lengths above 354 } 355 configEV := config.Index(idx) 356 priorEV := cty.NullVal(schema.ImpliedType()) 357 if !prior.IsNull() && prior.HasIndex(idx).True() { 358 priorEV = prior.Index(idx) 359 } 360 361 moreErrs := assertPlannedAttrsValid(schema.Attributes, priorEV, configEV, plannedEV, path) 362 errs = append(errs, moreErrs...) 363 } 364 365 case configschema.NestingMap: 366 // A NestingMap might either be a map or an object, depending on 367 // whether there are dynamically-typed attributes inside, so we will 368 // break these down to maps to handle them both in the same manner. 369 plannedVals := map[string]cty.Value{} 370 configVals := map[string]cty.Value{} 371 priorVals := map[string]cty.Value{} 372 373 if !planned.IsNull() { 374 plannedVals = planned.AsValueMap() 375 } 376 if !config.IsNull() { 377 configVals = config.AsValueMap() 378 } 379 if !prior.IsNull() { 380 priorVals = prior.AsValueMap() 381 } 382 383 for k, plannedEV := range plannedVals { 384 configEV, ok := configVals[k] 385 if !ok { 386 errs = append(errs, path.NewErrorf("map key %q from plan is not present in config", k)) 387 continue 388 } 389 path := append(path, cty.GetAttrStep{Name: k}) 390 391 priorEV, ok := priorVals[k] 392 if !ok { 393 priorEV = cty.NullVal(schema.ImpliedType()) 394 } 395 moreErrs := assertPlannedAttrsValid(schema.Attributes, priorEV, configEV, plannedEV, path) 396 errs = append(errs, moreErrs...) 397 } 398 for k := range configVals { 399 if _, ok := plannedVals[k]; !ok { 400 errs = append(errs, path.NewErrorf("map key %q from config is not present in plan", k)) 401 continue 402 } 403 } 404 405 case configschema.NestingSet: 406 plannedL := planned.LengthInt() 407 configL := config.LengthInt() 408 if plannedL != configL { 409 errs = append(errs, path.NewErrorf("count in plan (%d) disagrees with count in config (%d)", plannedL, configL)) 410 return errs 411 } 412 // Because set elements have no identifier with which to correlate 413 // them, we can't robustly validate the plan for a nested object 414 // backed by a set, and so unfortunately we need to just trust the 415 // provider to do the right thing. 416 } 417 418 return errs 419 }