github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/plans/objchange/plan_valid.go (about) 1 package objchange 2 3 import ( 4 "fmt" 5 6 "github.com/zclconf/go-cty/cty" 7 8 "github.com/hashicorp/terraform-plugin-sdk/internal/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 absense 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 absense")) 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 for name, attrS := range schema.Attributes { 57 plannedV := plannedState.GetAttr(name) 58 configV := config.GetAttr(name) 59 priorV := cty.NullVal(attrS.Type) 60 if !priorState.IsNull() { 61 priorV = priorState.GetAttr(name) 62 } 63 64 path := append(path, cty.GetAttrStep{Name: name}) 65 moreErrs := assertPlannedValueValid(attrS, priorV, configV, plannedV, path) 66 errs = append(errs, moreErrs...) 67 } 68 for name, blockS := range schema.BlockTypes { 69 path := append(path, cty.GetAttrStep{Name: name}) 70 plannedV := plannedState.GetAttr(name) 71 configV := config.GetAttr(name) 72 priorV := cty.NullVal(impTy.AttributeType(name)) 73 if !priorState.IsNull() { 74 priorV = priorState.GetAttr(name) 75 } 76 if plannedV.RawEquals(configV) { 77 // Easy path: nothing has changed at all 78 continue 79 } 80 if !plannedV.IsKnown() { 81 errs = append(errs, path.NewErrorf("attribute representing nested block must not be unknown itself; set nested attribute values to unknown instead")) 82 continue 83 } 84 85 switch blockS.Nesting { 86 case configschema.NestingSingle, configschema.NestingGroup: 87 moreErrs := assertPlanValid(&blockS.Block, priorV, configV, plannedV, 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.IsNull() { 95 errs = append(errs, path.NewErrorf("attribute representing a list of nested blocks must be empty to indicate no blocks, not null")) 96 continue 97 } 98 99 plannedL := plannedV.LengthInt() 100 configL := configV.LengthInt() 101 if plannedL != configL { 102 errs = append(errs, path.NewErrorf("block count in plan (%d) disagrees with count in config (%d)", plannedL, configL)) 103 continue 104 } 105 for it := plannedV.ElementIterator(); it.Next(); { 106 idx, plannedEV := it.Element() 107 path := append(path, cty.IndexStep{Key: idx}) 108 if !plannedEV.IsKnown() { 109 errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead")) 110 continue 111 } 112 if !configV.HasIndex(idx).True() { 113 continue // should never happen since we checked the lengths above 114 } 115 configEV := configV.Index(idx) 116 priorEV := cty.NullVal(blockS.ImpliedType()) 117 if !priorV.IsNull() && priorV.HasIndex(idx).True() { 118 priorEV = priorV.Index(idx) 119 } 120 121 moreErrs := assertPlanValid(&blockS.Block, priorEV, configEV, plannedEV, path) 122 errs = append(errs, moreErrs...) 123 } 124 case configschema.NestingMap: 125 if plannedV.IsNull() { 126 errs = append(errs, path.NewErrorf("attribute representing a map of nested blocks must be empty to indicate no blocks, not null")) 127 continue 128 } 129 130 // A NestingMap might either be a map or an object, depending on 131 // whether there are dynamically-typed attributes inside, but 132 // that's decided statically and so all values will have the same 133 // kind. 134 if plannedV.Type().IsObjectType() { 135 plannedAtys := plannedV.Type().AttributeTypes() 136 configAtys := configV.Type().AttributeTypes() 137 for k := range plannedAtys { 138 if _, ok := configAtys[k]; !ok { 139 errs = append(errs, path.NewErrorf("block key %q from plan is not present in config", k)) 140 continue 141 } 142 path := append(path, cty.GetAttrStep{Name: k}) 143 144 plannedEV := plannedV.GetAttr(k) 145 if !plannedEV.IsKnown() { 146 errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead")) 147 continue 148 } 149 configEV := configV.GetAttr(k) 150 priorEV := cty.NullVal(blockS.ImpliedType()) 151 if !priorV.IsNull() && priorV.Type().HasAttribute(k) { 152 priorEV = priorV.GetAttr(k) 153 } 154 moreErrs := assertPlanValid(&blockS.Block, priorEV, configEV, plannedEV, path) 155 errs = append(errs, moreErrs...) 156 } 157 for k := range configAtys { 158 if _, ok := plannedAtys[k]; !ok { 159 errs = append(errs, path.NewErrorf("block key %q from config is not present in plan", k)) 160 continue 161 } 162 } 163 } else { 164 plannedL := plannedV.LengthInt() 165 configL := configV.LengthInt() 166 if plannedL != configL { 167 errs = append(errs, path.NewErrorf("block count in plan (%d) disagrees with count in config (%d)", plannedL, configL)) 168 continue 169 } 170 for it := plannedV.ElementIterator(); it.Next(); { 171 idx, plannedEV := it.Element() 172 path := append(path, cty.IndexStep{Key: idx}) 173 if !plannedEV.IsKnown() { 174 errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead")) 175 continue 176 } 177 k := idx.AsString() 178 if !configV.HasIndex(idx).True() { 179 errs = append(errs, path.NewErrorf("block key %q from plan is not present in config", k)) 180 continue 181 } 182 configEV := configV.Index(idx) 183 priorEV := cty.NullVal(blockS.ImpliedType()) 184 if !priorV.IsNull() && priorV.HasIndex(idx).True() { 185 priorEV = priorV.Index(idx) 186 } 187 moreErrs := assertPlanValid(&blockS.Block, priorEV, configEV, plannedEV, path) 188 errs = append(errs, moreErrs...) 189 } 190 for it := configV.ElementIterator(); it.Next(); { 191 idx, _ := it.Element() 192 if !plannedV.HasIndex(idx).True() { 193 errs = append(errs, path.NewErrorf("block key %q from config is not present in plan", idx.AsString())) 194 continue 195 } 196 } 197 } 198 case configschema.NestingSet: 199 if plannedV.IsNull() { 200 errs = append(errs, path.NewErrorf("attribute representing a set of nested blocks must be empty to indicate no blocks, not null")) 201 continue 202 } 203 204 // Because set elements have no identifier with which to correlate 205 // them, we can't robustly validate the plan for a nested block 206 // backed by a set, and so unfortunately we need to just trust the 207 // provider to do the right thing. :( 208 // 209 // (In principle we could correlate elements by matching the 210 // subset of attributes explicitly set in config, except for the 211 // special diff suppression rule which allows for there to be a 212 // planned value that is constructed by mixing part of a prior 213 // value with part of a config value, creating an entirely new 214 // element that is not present in either prior nor config.) 215 for it := plannedV.ElementIterator(); it.Next(); { 216 idx, plannedEV := it.Element() 217 path := append(path, cty.IndexStep{Key: idx}) 218 if !plannedEV.IsKnown() { 219 errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead")) 220 continue 221 } 222 } 223 224 default: 225 panic(fmt.Sprintf("unsupported nesting mode %s", blockS.Nesting)) 226 } 227 } 228 229 return errs 230 } 231 232 func assertPlannedValueValid(attrS *configschema.Attribute, priorV, configV, plannedV cty.Value, path cty.Path) []error { 233 var errs []error 234 if plannedV.RawEquals(configV) { 235 // This is the easy path: provider didn't change anything at all. 236 return errs 237 } 238 if plannedV.RawEquals(priorV) && !priorV.IsNull() { 239 // Also pretty easy: there is a prior value and the provider has 240 // returned it unchanged. This indicates that configV and plannedV 241 // are functionally equivalent and so the provider wishes to disregard 242 // the configuration value in favor of the prior. 243 return errs 244 } 245 if attrS.Computed && configV.IsNull() { 246 // The provider is allowed to change the value of any computed 247 // attribute that isn't explicitly set in the config. 248 return errs 249 } 250 251 // If none of the above conditions match, the provider has made an invalid 252 // change to this attribute. 253 if priorV.IsNull() { 254 if attrS.Sensitive { 255 errs = append(errs, path.NewErrorf("sensitive planned value does not match config value")) 256 } else { 257 errs = append(errs, path.NewErrorf("planned value %#v does not match config value %#v", plannedV, configV)) 258 } 259 return errs 260 } 261 if attrS.Sensitive { 262 errs = append(errs, path.NewErrorf("sensitive planned value does not match config value nor prior value")) 263 } else { 264 errs = append(errs, path.NewErrorf("planned value %#v does not match config value %#v nor prior value %#v", plannedV, configV, priorV)) 265 } 266 return errs 267 }