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