github.com/opentofu/opentofu@v1.7.1/internal/states/statefile/version2_upgrade.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package statefile 7 8 import ( 9 "fmt" 10 "log" 11 "regexp" 12 "sort" 13 "strconv" 14 "strings" 15 16 "github.com/mitchellh/copystructure" 17 ) 18 19 func upgradeStateV2ToV3(old *stateV2) (*stateV3, error) { 20 if old == nil { 21 return (*stateV3)(nil), nil 22 } 23 24 var new *stateV3 25 { 26 copy, err := copystructure.Config{Lock: true}.Copy(old) 27 if err != nil { 28 panic(err) 29 } 30 newWrongType := copy.(*stateV2) 31 newRightType := (stateV3)(*newWrongType) 32 new = &newRightType 33 } 34 35 // Set the new version number 36 new.Version = 3 37 38 // Change the counts for things which look like maps to use the % 39 // syntax. Remove counts for empty collections - they will be added 40 // back in later. 41 for _, module := range new.Modules { 42 for _, resource := range module.Resources { 43 // Upgrade Primary 44 if resource.Primary != nil { 45 upgradeAttributesV2ToV3(resource.Primary) 46 } 47 48 // Upgrade Deposed 49 for _, deposed := range resource.Deposed { 50 upgradeAttributesV2ToV3(deposed) 51 } 52 } 53 } 54 55 return new, nil 56 } 57 58 func upgradeAttributesV2ToV3(instanceState *instanceStateV2) error { 59 collectionKeyRegexp := regexp.MustCompile(`^(.*\.)#$`) 60 collectionSubkeyRegexp := regexp.MustCompile(`^([^\.]+)\..*`) 61 62 // Identify the key prefix of anything which is a collection 63 var collectionKeyPrefixes []string 64 for key := range instanceState.Attributes { 65 if submatches := collectionKeyRegexp.FindAllStringSubmatch(key, -1); len(submatches) > 0 { 66 collectionKeyPrefixes = append(collectionKeyPrefixes, submatches[0][1]) 67 } 68 } 69 sort.Strings(collectionKeyPrefixes) 70 71 log.Printf("[STATE UPGRADE] Detected the following collections in state: %v", collectionKeyPrefixes) 72 73 // This could be rolled into fewer loops, but it is somewhat clearer this way, and will not 74 // run very often. 75 for _, prefix := range collectionKeyPrefixes { 76 // First get the actual keys that belong to this prefix 77 var potentialKeysMatching []string 78 for key := range instanceState.Attributes { 79 if strings.HasPrefix(key, prefix) { 80 potentialKeysMatching = append(potentialKeysMatching, strings.TrimPrefix(key, prefix)) 81 } 82 } 83 sort.Strings(potentialKeysMatching) 84 85 var actualKeysMatching []string 86 for _, key := range potentialKeysMatching { 87 if submatches := collectionSubkeyRegexp.FindAllStringSubmatch(key, -1); len(submatches) > 0 { 88 actualKeysMatching = append(actualKeysMatching, submatches[0][1]) 89 } else { 90 if key != "#" { 91 actualKeysMatching = append(actualKeysMatching, key) 92 } 93 } 94 } 95 actualKeysMatching = uniqueSortedStrings(actualKeysMatching) 96 97 // Now inspect the keys in order to determine whether this is most likely to be 98 // a map, list or set. There is room for error here, so we log in each case. If 99 // there is no method of telling, we remove the key from the InstanceState in 100 // order that it will be recreated. Again, this could be rolled into fewer loops 101 // but we prefer clarity. 102 103 oldCountKey := fmt.Sprintf("%s#", prefix) 104 105 // First, detect "obvious" maps - which have non-numeric keys (mostly). 106 hasNonNumericKeys := false 107 for _, key := range actualKeysMatching { 108 if _, err := strconv.Atoi(key); err != nil { 109 hasNonNumericKeys = true 110 } 111 } 112 if hasNonNumericKeys { 113 newCountKey := fmt.Sprintf("%s%%", prefix) 114 115 instanceState.Attributes[newCountKey] = instanceState.Attributes[oldCountKey] 116 delete(instanceState.Attributes, oldCountKey) 117 log.Printf("[STATE UPGRADE] Detected %s as a map. Replaced count = %s", 118 strings.TrimSuffix(prefix, "."), instanceState.Attributes[newCountKey]) 119 } 120 121 // Now detect empty collections and remove them from state. 122 if len(actualKeysMatching) == 0 { 123 delete(instanceState.Attributes, oldCountKey) 124 log.Printf("[STATE UPGRADE] Detected %s as an empty collection. Removed from state.", 125 strings.TrimSuffix(prefix, ".")) 126 } 127 } 128 129 return nil 130 } 131 132 // uniqueSortedStrings removes duplicates from a slice of strings and returns 133 // a sorted slice of the unique strings. 134 func uniqueSortedStrings(input []string) []string { 135 uniquemap := make(map[string]struct{}) 136 for _, str := range input { 137 uniquemap[str] = struct{}{} 138 } 139 140 output := make([]string, len(uniquemap)) 141 142 i := 0 143 for key := range uniquemap { 144 output[i] = key 145 i = i + 1 146 } 147 148 sort.Strings(output) 149 return output 150 }