1package statefile
2
3import (
4	"fmt"
5	"log"
6	"regexp"
7	"sort"
8	"strconv"
9	"strings"
10
11	"github.com/mitchellh/copystructure"
12)
13
14func upgradeStateV2ToV3(old *stateV2) (*stateV3, error) {
15	if old == nil {
16		return (*stateV3)(nil), nil
17	}
18
19	var new *stateV3
20	{
21		copy, err := copystructure.Config{Lock: true}.Copy(old)
22		if err != nil {
23			panic(err)
24		}
25		newWrongType := copy.(*stateV2)
26		newRightType := (stateV3)(*newWrongType)
27		new = &newRightType
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			for _, deposed := range resource.Deposed {
45				upgradeAttributesV2ToV3(deposed)
46			}
47		}
48	}
49
50	return new, nil
51}
52
53func upgradeAttributesV2ToV3(instanceState *instanceStateV2) 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.
129func 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}
146