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