github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/remote-state/inmem/backend.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package inmem 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "sort" 11 "sync" 12 "time" 13 14 "github.com/terramate-io/tf/backend" 15 "github.com/terramate-io/tf/legacy/helper/schema" 16 statespkg "github.com/terramate-io/tf/states" 17 "github.com/terramate-io/tf/states/remote" 18 "github.com/terramate-io/tf/states/statemgr" 19 ) 20 21 // we keep the states and locks in package-level variables, so that they can be 22 // accessed from multiple instances of the backend. This better emulates 23 // backend instances accessing a single remote data store. 24 var ( 25 states stateMap 26 locks lockMap 27 ) 28 29 func init() { 30 Reset() 31 } 32 33 // Reset clears out all existing state and lock data. 34 // This is used to initialize the package during init, as well as between 35 // tests. 36 func Reset() { 37 states = stateMap{ 38 m: map[string]*remote.State{}, 39 } 40 41 locks = lockMap{ 42 m: map[string]*statemgr.LockInfo{}, 43 } 44 } 45 46 // New creates a new backend for Inmem remote state. 47 func New() backend.Backend { 48 // Set the schema 49 s := &schema.Backend{ 50 Schema: map[string]*schema.Schema{ 51 "lock_id": &schema.Schema{ 52 Type: schema.TypeString, 53 Optional: true, 54 Description: "initializes the state in a locked configuration", 55 }, 56 }, 57 } 58 backend := &Backend{Backend: s} 59 backend.Backend.ConfigureFunc = backend.configure 60 return backend 61 } 62 63 type Backend struct { 64 *schema.Backend 65 } 66 67 func (b *Backend) configure(ctx context.Context) error { 68 states.Lock() 69 defer states.Unlock() 70 71 defaultClient := &RemoteClient{ 72 Name: backend.DefaultStateName, 73 } 74 75 states.m[backend.DefaultStateName] = &remote.State{ 76 Client: defaultClient, 77 } 78 79 // set the default client lock info per the test config 80 data := schema.FromContextBackendConfig(ctx) 81 if v, ok := data.GetOk("lock_id"); ok && v.(string) != "" { 82 info := statemgr.NewLockInfo() 83 info.ID = v.(string) 84 info.Operation = "test" 85 info.Info = "test config" 86 87 locks.lock(backend.DefaultStateName, info) 88 } 89 90 return nil 91 } 92 93 func (b *Backend) Workspaces() ([]string, error) { 94 states.Lock() 95 defer states.Unlock() 96 97 var workspaces []string 98 99 for s := range states.m { 100 workspaces = append(workspaces, s) 101 } 102 103 sort.Strings(workspaces) 104 return workspaces, nil 105 } 106 107 func (b *Backend) DeleteWorkspace(name string, _ bool) error { 108 states.Lock() 109 defer states.Unlock() 110 111 if name == backend.DefaultStateName || name == "" { 112 return fmt.Errorf("can't delete default state") 113 } 114 115 delete(states.m, name) 116 return nil 117 } 118 119 func (b *Backend) StateMgr(name string) (statemgr.Full, error) { 120 states.Lock() 121 defer states.Unlock() 122 123 s := states.m[name] 124 if s == nil { 125 s = &remote.State{ 126 Client: &RemoteClient{ 127 Name: name, 128 }, 129 } 130 states.m[name] = s 131 132 // to most closely replicate other implementations, we are going to 133 // take a lock and create a new state if it doesn't exist. 134 lockInfo := statemgr.NewLockInfo() 135 lockInfo.Operation = "init" 136 lockID, err := s.Lock(lockInfo) 137 if err != nil { 138 return nil, fmt.Errorf("failed to lock inmem state: %s", err) 139 } 140 defer s.Unlock(lockID) 141 142 // If we have no state, we have to create an empty state 143 if v := s.State(); v == nil { 144 if err := s.WriteState(statespkg.NewState()); err != nil { 145 return nil, err 146 } 147 if err := s.PersistState(nil); err != nil { 148 return nil, err 149 } 150 } 151 } 152 153 return s, nil 154 } 155 156 type stateMap struct { 157 sync.Mutex 158 m map[string]*remote.State 159 } 160 161 // Global level locks for inmem backends. 162 type lockMap struct { 163 sync.Mutex 164 m map[string]*statemgr.LockInfo 165 } 166 167 func (l *lockMap) lock(name string, info *statemgr.LockInfo) (string, error) { 168 l.Lock() 169 defer l.Unlock() 170 171 lockInfo := l.m[name] 172 if lockInfo != nil { 173 lockErr := &statemgr.LockError{ 174 Info: lockInfo, 175 } 176 177 lockErr.Err = errors.New("state locked") 178 // make a copy of the lock info to avoid any testing shenanigans 179 *lockErr.Info = *lockInfo 180 return "", lockErr 181 } 182 183 info.Created = time.Now().UTC() 184 l.m[name] = info 185 186 return info.ID, nil 187 } 188 189 func (l *lockMap) unlock(name, id string) error { 190 l.Lock() 191 defer l.Unlock() 192 193 lockInfo := l.m[name] 194 195 if lockInfo == nil { 196 return errors.New("state not locked") 197 } 198 199 lockErr := &statemgr.LockError{ 200 Info: &statemgr.LockInfo{}, 201 } 202 203 if id != lockInfo.ID { 204 lockErr.Err = errors.New("invalid lock id") 205 *lockErr.Info = *lockInfo 206 return lockErr 207 } 208 209 delete(l.m, name) 210 return nil 211 }