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