github.com/opentofu/opentofu@v1.7.1/internal/states/statefile/version2_upgrade.go (about)

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