github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/states/statefile/version3_upgrade.go (about) 1 package statefile 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "strconv" 7 "strings" 8 9 "github.com/zclconf/go-cty/cty" 10 ctyjson "github.com/zclconf/go-cty/cty/json" 11 12 "github.com/hashicorp/terraform-plugin-sdk/internal/addrs" 13 "github.com/hashicorp/terraform-plugin-sdk/internal/states" 14 "github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags" 15 ) 16 17 func upgradeStateV3ToV4(old *stateV3) (*stateV4, error) { 18 19 if old.Serial < 0 { 20 // The new format is using uint64 here, which should be fine for any 21 // real state (we only used positive integers in practice) but we'll 22 // catch this explicitly here to avoid weird behavior if a state file 23 // has been tampered with in some way. 24 return nil, fmt.Errorf("state has serial less than zero, which is invalid") 25 } 26 27 new := &stateV4{ 28 TerraformVersion: old.TFVersion, 29 Serial: uint64(old.Serial), 30 Lineage: old.Lineage, 31 RootOutputs: map[string]outputStateV4{}, 32 Resources: []resourceStateV4{}, 33 } 34 35 if new.TerraformVersion == "" { 36 // Older formats considered this to be optional, but now it's required 37 // and so we'll stub it out with something that's definitely older 38 // than the version that really created this state. 39 new.TerraformVersion = "0.0.0" 40 } 41 42 for _, msOld := range old.Modules { 43 if len(msOld.Path) < 1 || msOld.Path[0] != "root" { 44 return nil, fmt.Errorf("state contains invalid module path %#v", msOld.Path) 45 } 46 47 // Convert legacy-style module address into our newer address type. 48 // Since these old formats are only generated by versions of Terraform 49 // that don't support count and for_each on modules, we can just assume 50 // all of the modules are unkeyed. 51 moduleAddr := make(addrs.ModuleInstance, len(msOld.Path)-1) 52 for i, name := range msOld.Path[1:] { 53 moduleAddr[i] = addrs.ModuleInstanceStep{ 54 Name: name, 55 InstanceKey: addrs.NoKey, 56 } 57 } 58 59 // In a v3 state file, a "resource state" is actually an instance 60 // state, so we need to fill in a missing level of heirarchy here 61 // by lazily creating resource states as we encounter them. 62 // We'll track them in here, keyed on the string representation of 63 // the resource address. 64 resourceStates := map[string]*resourceStateV4{} 65 66 for legacyAddr, rsOld := range msOld.Resources { 67 instAddr, err := parseLegacyResourceAddress(legacyAddr) 68 if err != nil { 69 return nil, err 70 } 71 72 resAddr := instAddr.Resource 73 rs, exists := resourceStates[resAddr.String()] 74 if !exists { 75 var modeStr string 76 switch resAddr.Mode { 77 case addrs.ManagedResourceMode: 78 modeStr = "managed" 79 case addrs.DataResourceMode: 80 modeStr = "data" 81 default: 82 return nil, fmt.Errorf("state contains resource %s with an unsupported resource mode %#v", resAddr, resAddr.Mode) 83 } 84 85 // In state versions prior to 4 we allowed each instance of a 86 // resource to have its own provider configuration address, 87 // which makes no real sense in practice because providers 88 // are associated with resources in the configuration. We 89 // elevate that to the resource level during this upgrade, 90 // implicitly taking the provider address of the first instance 91 // we encounter for each resource. While this is lossy in 92 // theory, in practice there is no reason for these values to 93 // differ between instances. 94 var providerAddr addrs.AbsProviderConfig 95 oldProviderAddr := rsOld.Provider 96 if strings.Contains(oldProviderAddr, "provider.") { 97 // Smells like a new-style provider address, but we'll test it. 98 var diags tfdiags.Diagnostics 99 providerAddr, diags = addrs.ParseAbsProviderConfigStr(oldProviderAddr) 100 if diags.HasErrors() { 101 return nil, fmt.Errorf("invalid provider config reference %q for %s: %s", oldProviderAddr, instAddr, diags.Err()) 102 } 103 } else { 104 // Smells like an old-style module-local provider address, 105 // which we'll need to migrate. We'll assume it's referring 106 // to the same module the resource is in, which might be 107 // incorrect but it'll get fixed up next time any updates 108 // are made to an instance. 109 if oldProviderAddr != "" { 110 localAddr, diags := addrs.ParseProviderConfigCompactStr(oldProviderAddr) 111 if diags.HasErrors() { 112 return nil, fmt.Errorf("invalid legacy provider config reference %q for %s: %s", oldProviderAddr, instAddr, diags.Err()) 113 } 114 providerAddr = localAddr.Absolute(moduleAddr) 115 } else { 116 providerAddr = resAddr.DefaultProviderConfig().Absolute(moduleAddr) 117 } 118 } 119 120 rs = &resourceStateV4{ 121 Module: moduleAddr.String(), 122 Mode: modeStr, 123 Type: resAddr.Type, 124 Name: resAddr.Name, 125 Instances: []instanceObjectStateV4{}, 126 ProviderConfig: providerAddr.String(), 127 } 128 resourceStates[resAddr.String()] = rs 129 } 130 131 // Now we'll deal with the instance itself, which may either be 132 // the first instance in a resource we just created or an additional 133 // instance for a resource added on a prior loop. 134 instKey := instAddr.Key 135 if isOld := rsOld.Primary; isOld != nil { 136 isNew, err := upgradeInstanceObjectV3ToV4(rsOld, isOld, instKey, states.NotDeposed) 137 if err != nil { 138 return nil, fmt.Errorf("failed to migrate primary generation of %s: %s", instAddr, err) 139 } 140 rs.Instances = append(rs.Instances, *isNew) 141 } 142 for i, isOld := range rsOld.Deposed { 143 // When we migrate old instances we'll use sequential deposed 144 // keys just so that the upgrade result is deterministic. New 145 // deposed keys allocated moving forward will be pseudorandomly 146 // selected, but we check for collisions and so these 147 // non-random ones won't hurt. 148 deposedKey := states.DeposedKey(fmt.Sprintf("%08x", i+1)) 149 isNew, err := upgradeInstanceObjectV3ToV4(rsOld, isOld, instKey, deposedKey) 150 if err != nil { 151 return nil, fmt.Errorf("failed to migrate deposed generation index %d of %s: %s", i, instAddr, err) 152 } 153 rs.Instances = append(rs.Instances, *isNew) 154 } 155 156 if instKey != addrs.NoKey && rs.EachMode == "" { 157 rs.EachMode = "list" 158 } 159 } 160 161 for _, rs := range resourceStates { 162 new.Resources = append(new.Resources, *rs) 163 } 164 165 if len(msOld.Path) == 1 && msOld.Path[0] == "root" { 166 // We'll migrate the outputs for this module too, then. 167 for name, oldOS := range msOld.Outputs { 168 newOS := outputStateV4{ 169 Sensitive: oldOS.Sensitive, 170 } 171 172 valRaw := oldOS.Value 173 valSrc, err := json.Marshal(valRaw) 174 if err != nil { 175 // Should never happen, because this value came from JSON 176 // in the first place and so we're just round-tripping here. 177 return nil, fmt.Errorf("failed to serialize output %q value as JSON: %s", name, err) 178 } 179 180 // The "type" field in state V2 wasn't really that useful 181 // since it was only able to capture string vs. list vs. map. 182 // For this reason, during upgrade we'll just discard it 183 // altogether and use cty's idea of the implied type of 184 // turning our old value into JSON. 185 ty, err := ctyjson.ImpliedType(valSrc) 186 if err != nil { 187 // REALLY should never happen, because we literally just 188 // encoded this as JSON above! 189 return nil, fmt.Errorf("failed to parse output %q value from JSON: %s", name, err) 190 } 191 192 // ImpliedType tends to produce structural types, but since older 193 // version of Terraform didn't support those a collection type 194 // is probably what was intended, so we'll see if we can 195 // interpret our value as one. 196 ty = simplifyImpliedValueType(ty) 197 198 tySrc, err := ctyjson.MarshalType(ty) 199 if err != nil { 200 return nil, fmt.Errorf("failed to serialize output %q type as JSON: %s", name, err) 201 } 202 203 newOS.ValueRaw = json.RawMessage(valSrc) 204 newOS.ValueTypeRaw = json.RawMessage(tySrc) 205 206 new.RootOutputs[name] = newOS 207 } 208 } 209 } 210 211 new.normalize() 212 213 return new, nil 214 } 215 216 func upgradeInstanceObjectV3ToV4(rsOld *resourceStateV2, isOld *instanceStateV2, instKey addrs.InstanceKey, deposedKey states.DeposedKey) (*instanceObjectStateV4, error) { 217 218 // Schema versions were, in prior formats, a private concern of the provider 219 // SDK, and not a first-class concept in the state format. Here we're 220 // sniffing for the pre-0.12 SDK's way of representing schema versions 221 // and promoting it to our first-class field if we find it. We'll ignore 222 // it if it doesn't look like what the SDK would've written. If this 223 // sniffing fails then we'll assume schema version 0. 224 var schemaVersion uint64 225 migratedSchemaVersion := false 226 if raw, exists := isOld.Meta["schema_version"]; exists { 227 switch tv := raw.(type) { 228 case string: 229 v, err := strconv.ParseUint(tv, 10, 64) 230 if err == nil { 231 schemaVersion = v 232 migratedSchemaVersion = true 233 } 234 case int: 235 schemaVersion = uint64(tv) 236 migratedSchemaVersion = true 237 case float64: 238 schemaVersion = uint64(tv) 239 migratedSchemaVersion = true 240 } 241 } 242 243 private := map[string]interface{}{} 244 for k, v := range isOld.Meta { 245 if k == "schema_version" && migratedSchemaVersion { 246 // We're gonna promote this into our first-class schema version field 247 continue 248 } 249 private[k] = v 250 } 251 var privateJSON []byte 252 if len(private) != 0 { 253 var err error 254 privateJSON, err = json.Marshal(private) 255 if err != nil { 256 // This shouldn't happen, because the Meta values all came from JSON 257 // originally anyway. 258 return nil, fmt.Errorf("cannot serialize private instance object data: %s", err) 259 } 260 } 261 262 var status string 263 if isOld.Tainted { 264 status = "tainted" 265 } 266 267 var instKeyRaw interface{} 268 switch tk := instKey.(type) { 269 case addrs.IntKey: 270 instKeyRaw = int(tk) 271 case addrs.StringKey: 272 instKeyRaw = string(tk) 273 default: 274 if instKeyRaw != nil { 275 return nil, fmt.Errorf("unsupported instance key: %#v", instKey) 276 } 277 } 278 279 var attributes map[string]string 280 if isOld.Attributes != nil { 281 attributes = make(map[string]string, len(isOld.Attributes)) 282 for k, v := range isOld.Attributes { 283 attributes[k] = v 284 } 285 } 286 if isOld.ID != "" { 287 // As a special case, if we don't already have an "id" attribute and 288 // yet there's a non-empty first-class ID on the old object then we'll 289 // create a synthetic id attribute to avoid losing that first-class id. 290 // In practice this generally arises only in tests where state literals 291 // are hand-written in a non-standard way; real code prior to 0.12 292 // would always force the first-class ID to be copied into the 293 // id attribute before storing. 294 if attributes == nil { 295 attributes = make(map[string]string, len(isOld.Attributes)) 296 } 297 if idVal := attributes["id"]; idVal == "" { 298 attributes["id"] = isOld.ID 299 } 300 } 301 302 dependencies := make([]string, len(rsOld.Dependencies)) 303 for i, v := range rsOld.Dependencies { 304 depStr, err := parseLegacyDependency(v) 305 if err != nil { 306 return nil, fmt.Errorf("invalid dependency reference %q: %s", v, err) 307 } 308 dependencies[i] = depStr 309 } 310 311 return &instanceObjectStateV4{ 312 IndexKey: instKeyRaw, 313 Status: status, 314 Deposed: string(deposedKey), 315 AttributesFlat: attributes, 316 Dependencies: dependencies, 317 SchemaVersion: schemaVersion, 318 PrivateRaw: privateJSON, 319 }, nil 320 } 321 322 // parseLegacyResourceAddress parses the different identifier format used 323 // state formats before version 4, like "instance.name.0". 324 func parseLegacyResourceAddress(s string) (addrs.ResourceInstance, error) { 325 var ret addrs.ResourceInstance 326 327 // Split based on ".". Every resource address should have at least two 328 // elements (type and name). 329 parts := strings.Split(s, ".") 330 if len(parts) < 2 || len(parts) > 4 { 331 return ret, fmt.Errorf("invalid internal resource address format: %s", s) 332 } 333 334 // Data resource if we have at least 3 parts and the first one is data 335 ret.Resource.Mode = addrs.ManagedResourceMode 336 if len(parts) > 2 && parts[0] == "data" { 337 ret.Resource.Mode = addrs.DataResourceMode 338 parts = parts[1:] 339 } 340 341 // If we're not a data resource and we have more than 3, then it is an error 342 if len(parts) > 3 && ret.Resource.Mode != addrs.DataResourceMode { 343 return ret, fmt.Errorf("invalid internal resource address format: %s", s) 344 } 345 346 // Build the parts of the resource address that are guaranteed to exist 347 ret.Resource.Type = parts[0] 348 ret.Resource.Name = parts[1] 349 ret.Key = addrs.NoKey 350 351 // If we have more parts, then we have an index. Parse that. 352 if len(parts) > 2 { 353 idx, err := strconv.ParseInt(parts[2], 0, 0) 354 if err != nil { 355 return ret, fmt.Errorf("error parsing resource address %q: %s", s, err) 356 } 357 358 ret.Key = addrs.IntKey(idx) 359 } 360 361 return ret, nil 362 } 363 364 // simplifyImpliedValueType attempts to heuristically simplify a value type 365 // derived from a legacy stored output value into something simpler that 366 // is closer to what would've fitted into the pre-v0.12 value type system. 367 func simplifyImpliedValueType(ty cty.Type) cty.Type { 368 switch { 369 case ty.IsTupleType(): 370 // If all of the element types are the same then we'll make this 371 // a list instead. This is very likely to be true, since prior versions 372 // of Terraform did not officially support mixed-type collections. 373 374 if ty.Equals(cty.EmptyTuple) { 375 // Don't know what the element type would be, then. 376 return ty 377 } 378 379 etys := ty.TupleElementTypes() 380 ety := etys[0] 381 for _, other := range etys[1:] { 382 if !other.Equals(ety) { 383 // inconsistent types 384 return ty 385 } 386 } 387 ety = simplifyImpliedValueType(ety) 388 return cty.List(ety) 389 390 case ty.IsObjectType(): 391 // If all of the attribute types are the same then we'll make this 392 // a map instead. This is very likely to be true, since prior versions 393 // of Terraform did not officially support mixed-type collections. 394 395 if ty.Equals(cty.EmptyObject) { 396 // Don't know what the element type would be, then. 397 return ty 398 } 399 400 atys := ty.AttributeTypes() 401 var ety cty.Type 402 for _, other := range atys { 403 if ety == cty.NilType { 404 ety = other 405 continue 406 } 407 if !other.Equals(ety) { 408 // inconsistent types 409 return ty 410 } 411 } 412 ety = simplifyImpliedValueType(ety) 413 return cty.Map(ety) 414 415 default: 416 // No other normalizations are possible 417 return ty 418 } 419 } 420 421 func parseLegacyDependency(s string) (string, error) { 422 parts := strings.Split(s, ".") 423 ret := parts[0] 424 for _, part := range parts[1:] { 425 if part == "*" { 426 break 427 } 428 if i, err := strconv.Atoi(part); err == nil { 429 ret = ret + fmt.Sprintf("[%d]", i) 430 break 431 } 432 ret = ret + "." + part 433 } 434 435 // The result must parse as a reference, or else we'll create an invalid 436 // state file. 437 var diags tfdiags.Diagnostics 438 _, diags = addrs.ParseRefStr(ret) 439 if diags.HasErrors() { 440 return "", diags.Err() 441 } 442 443 return ret, nil 444 }