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