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