github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/consul/backend_state.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 consul 7 8 import ( 9 "fmt" 10 "strings" 11 12 "github.com/opentofu/opentofu/internal/backend" 13 "github.com/opentofu/opentofu/internal/states" 14 "github.com/opentofu/opentofu/internal/states/remote" 15 "github.com/opentofu/opentofu/internal/states/statemgr" 16 ) 17 18 const ( 19 keyEnvPrefix = "-env:" 20 ) 21 22 func (b *Backend) Workspaces() ([]string, error) { 23 // List our raw path 24 prefix := b.configData.Get("path").(string) + keyEnvPrefix 25 keys, _, err := b.client.KV().Keys(prefix, "/", nil) 26 if err != nil { 27 return nil, err 28 } 29 30 // Find the envs, we use a map since we can get duplicates with 31 // path suffixes. 32 envs := map[string]struct{}{} 33 for _, key := range keys { 34 // Consul should ensure this but it doesn't hurt to check again 35 if strings.HasPrefix(key, prefix) { 36 key = strings.TrimPrefix(key, prefix) 37 38 // Ignore anything with a "/" in it since we store the state 39 // directly in a key not a directory. 40 if idx := strings.IndexRune(key, '/'); idx >= 0 { 41 continue 42 } 43 44 envs[key] = struct{}{} 45 } 46 } 47 48 result := make([]string, 1, len(envs)+1) 49 result[0] = backend.DefaultStateName 50 for k, _ := range envs { 51 result = append(result, k) 52 } 53 54 return result, nil 55 } 56 57 func (b *Backend) DeleteWorkspace(name string, _ bool) error { 58 if name == backend.DefaultStateName || name == "" { 59 return fmt.Errorf("can't delete default state") 60 } 61 62 // Determine the path of the data 63 path := b.path(name) 64 65 // Delete it. We just delete it without any locking since 66 // the DeleteState API is documented as such. 67 _, err := b.client.KV().Delete(path, nil) 68 return err 69 } 70 71 func (b *Backend) StateMgr(name string) (statemgr.Full, error) { 72 // Determine the path of the data 73 path := b.path(name) 74 75 // Determine whether to gzip or not 76 gzip := b.configData.Get("gzip").(bool) 77 78 // Build the state client 79 var stateMgr = remote.NewState( 80 &RemoteClient{ 81 Client: b.client, 82 Path: path, 83 GZip: gzip, 84 lockState: b.lock, 85 }, 86 b.encryption, 87 ) 88 89 if !b.lock { 90 stateMgr.DisableLocks() 91 } 92 93 // the default state always exists 94 if name == backend.DefaultStateName { 95 return stateMgr, nil 96 } 97 98 // Grab a lock, we use this to write an empty state if one doesn't 99 // exist already. We have to write an empty state as a sentinel value 100 // so States() knows it exists. 101 lockInfo := statemgr.NewLockInfo() 102 lockInfo.Operation = "init" 103 lockId, err := stateMgr.Lock(lockInfo) 104 if err != nil { 105 return nil, fmt.Errorf("failed to lock state in Consul: %w", err) 106 } 107 108 // Local helper function so we can call it multiple places 109 lockUnlock := func(parent error) error { 110 if err := stateMgr.Unlock(lockId); err != nil { 111 return fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err) 112 } 113 114 return parent 115 } 116 117 // Grab the value 118 if err := stateMgr.RefreshState(); err != nil { 119 err = lockUnlock(err) 120 return nil, err 121 } 122 123 // If we have no state, we have to create an empty state 124 if v := stateMgr.State(); v == nil { 125 if err := stateMgr.WriteState(states.NewState()); err != nil { 126 err = lockUnlock(err) 127 return nil, err 128 } 129 if err := stateMgr.PersistState(nil); err != nil { 130 err = lockUnlock(err) 131 return nil, err 132 } 133 } 134 135 // Unlock, the state should now be initialized 136 if err := lockUnlock(nil); err != nil { 137 return nil, err 138 } 139 140 return stateMgr, nil 141 } 142 143 func (b *Backend) path(name string) string { 144 path := b.configData.Get("path").(string) 145 if name != backend.DefaultStateName { 146 path += fmt.Sprintf("%s%s", keyEnvPrefix, name) 147 } 148 149 return path 150 } 151 152 const errStateUnlock = ` 153 Error unlocking Consul state. Lock ID: %s 154 155 Error: %w 156 157 You may have to force-unlock this state in order to use it again. 158 The Consul backend acquires a lock during initialization to ensure 159 the minimum required key/values are prepared. 160 `