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  }