github.com/opentofu/opentofu@v1.7.1/internal/legacy/tofu/state_upgrade_v2_to_v3.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 tofu 7 8 import ( 9 "fmt" 10 "log" 11 "regexp" 12 "sort" 13 "strconv" 14 "strings" 15 ) 16 17 // The upgrade process from V2 to V3 state does not affect the structure, 18 // so we do not need to redeclare all of the structs involved - we just 19 // take a deep copy of the old structure and assert the version number is 20 // as we expect. 21 func upgradeStateV2ToV3(old *State) (*State, error) { 22 new := old.DeepCopy() 23 24 // Ensure the copied version is v2 before attempting to upgrade 25 if new.Version != 2 { 26 return nil, fmt.Errorf("Cannot apply v2->v3 state upgrade to " + 27 "a state which is not version 2.") 28 } 29 30 // Set the new version number 31 new.Version = 3 32 33 // Change the counts for things which look like maps to use the % 34 // syntax. Remove counts for empty collections - they will be added 35 // back in later. 36 for _, module := range new.Modules { 37 for _, resource := range module.Resources { 38 // Upgrade Primary 39 if resource.Primary != nil { 40 upgradeAttributesV2ToV3(resource.Primary) 41 } 42 43 // Upgrade Deposed 44 if resource.Deposed != nil { 45 for _, deposed := range resource.Deposed { 46 upgradeAttributesV2ToV3(deposed) 47 } 48 } 49 } 50 } 51 52 return new, nil 53 } 54 55 func upgradeAttributesV2ToV3(instanceState *InstanceState) error { 56 collectionKeyRegexp := regexp.MustCompile(`^(.*\.)#$`) 57 collectionSubkeyRegexp := regexp.MustCompile(`^([^\.]+)\..*`) 58 59 // Identify the key prefix of anything which is a collection 60 var collectionKeyPrefixes []string 61 for key := range instanceState.Attributes { 62 if submatches := collectionKeyRegexp.FindAllStringSubmatch(key, -1); len(submatches) > 0 { 63 collectionKeyPrefixes = append(collectionKeyPrefixes, submatches[0][1]) 64 } 65 } 66 sort.Strings(collectionKeyPrefixes) 67 68 log.Printf("[STATE UPGRADE] Detected the following collections in state: %v", collectionKeyPrefixes) 69 70 // This could be rolled into fewer loops, but it is somewhat clearer this way, and will not 71 // run very often. 72 for _, prefix := range collectionKeyPrefixes { 73 // First get the actual keys that belong to this prefix 74 var potentialKeysMatching []string 75 for key := range instanceState.Attributes { 76 if strings.HasPrefix(key, prefix) { 77 potentialKeysMatching = append(potentialKeysMatching, strings.TrimPrefix(key, prefix)) 78 } 79 } 80 sort.Strings(potentialKeysMatching) 81 82 var actualKeysMatching []string 83 for _, key := range potentialKeysMatching { 84 if submatches := collectionSubkeyRegexp.FindAllStringSubmatch(key, -1); len(submatches) > 0 { 85 actualKeysMatching = append(actualKeysMatching, submatches[0][1]) 86 } else { 87 if key != "#" { 88 actualKeysMatching = append(actualKeysMatching, key) 89 } 90 } 91 } 92 actualKeysMatching = uniqueSortedStrings(actualKeysMatching) 93 94 // Now inspect the keys in order to determine whether this is most likely to be 95 // a map, list or set. There is room for error here, so we log in each case. If 96 // there is no method of telling, we remove the key from the InstanceState in 97 // order that it will be recreated. Again, this could be rolled into fewer loops 98 // but we prefer clarity. 99 100 oldCountKey := fmt.Sprintf("%s#", prefix) 101 102 // First, detect "obvious" maps - which have non-numeric keys (mostly). 103 hasNonNumericKeys := false 104 for _, key := range actualKeysMatching { 105 // Ensure that we attempt to parse the key using 64 bits, this is because the state 106 // could've been generated on a 64-bit system, and we need to be able to 107 // convert this on both a 32-bit and 64-bit arch. 108 if _, err := strconv.ParseInt(key, 10, 64); 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 }