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