github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/command/clistate/local_state.go (about) 1 package clistate 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "path/filepath" 11 "sync" 12 "time" 13 14 multierror "github.com/hashicorp/go-multierror" 15 "github.com/iaas-resource-provision/iaas-rpc/internal/legacy/terraform" 16 "github.com/iaas-resource-provision/iaas-rpc/internal/states/statemgr" 17 ) 18 19 // LocalState manages a state storage that is local to the filesystem. 20 type LocalState struct { 21 mu sync.Mutex 22 23 // Path is the path to read the state from. PathOut is the path to 24 // write the state to. If PathOut is not specified, Path will be used. 25 // If PathOut already exists, it will be overwritten. 26 Path string 27 PathOut string 28 29 // the file handle corresponding to PathOut 30 stateFileOut *os.File 31 32 // While the stateFileOut will correspond to the lock directly, 33 // store and check the lock ID to maintain a strict state.Locker 34 // implementation. 35 lockID string 36 37 // created is set to true if stateFileOut didn't exist before we created it. 38 // This is mostly so we can clean up empty files during tests, but doesn't 39 // hurt to remove file we never wrote to. 40 created bool 41 42 state *terraform.State 43 readState *terraform.State 44 written bool 45 } 46 47 // SetState will force a specific state in-memory for this local state. 48 func (s *LocalState) SetState(state *terraform.State) { 49 s.mu.Lock() 50 defer s.mu.Unlock() 51 52 s.state = state.DeepCopy() 53 s.readState = state.DeepCopy() 54 } 55 56 // StateReader impl. 57 func (s *LocalState) State() *terraform.State { 58 return s.state.DeepCopy() 59 } 60 61 // WriteState for LocalState always persists the state as well. 62 // TODO: this should use a more robust method of writing state, by first 63 // writing to a temp file on the same filesystem, and renaming the file over 64 // the original. 65 // 66 // StateWriter impl. 67 func (s *LocalState) WriteState(state *terraform.State) error { 68 s.mu.Lock() 69 defer s.mu.Unlock() 70 71 if s.stateFileOut == nil { 72 if err := s.createStateFiles(); err != nil { 73 return nil 74 } 75 } 76 defer s.stateFileOut.Sync() 77 78 s.state = state.DeepCopy() // don't want mutations before we actually get this written to disk 79 80 if s.readState != nil && s.state != nil { 81 // We don't trust callers to properly manage serials. Instead, we assume 82 // that a WriteState is always for the next version after what was 83 // most recently read. 84 s.state.Serial = s.readState.Serial 85 } 86 87 if _, err := s.stateFileOut.Seek(0, io.SeekStart); err != nil { 88 return err 89 } 90 if err := s.stateFileOut.Truncate(0); err != nil { 91 return err 92 } 93 94 if state == nil { 95 // if we have no state, don't write anything else. 96 return nil 97 } 98 99 if !s.state.MarshalEqual(s.readState) { 100 s.state.Serial++ 101 } 102 103 if err := terraform.WriteState(s.state, s.stateFileOut); err != nil { 104 return err 105 } 106 107 s.written = true 108 return nil 109 } 110 111 // PersistState for LocalState is a no-op since WriteState always persists. 112 // 113 // StatePersister impl. 114 func (s *LocalState) PersistState() error { 115 return nil 116 } 117 118 // StateRefresher impl. 119 func (s *LocalState) RefreshState() error { 120 s.mu.Lock() 121 defer s.mu.Unlock() 122 123 if s.PathOut == "" { 124 s.PathOut = s.Path 125 } 126 127 var reader io.Reader 128 129 // The s.Path file is only OK to read if we have not written any state out 130 // (in which case the same state needs to be read in), and no state output file 131 // has been opened (possibly via a lock) or the input path is different 132 // than the output path. 133 // This is important for Windows, as if the input file is the same as the 134 // output file, and the output file has been locked already, we can't open 135 // the file again. 136 if !s.written && (s.stateFileOut == nil || s.Path != s.PathOut) { 137 // we haven't written a state file yet, so load from Path 138 f, err := os.Open(s.Path) 139 if err != nil { 140 // It is okay if the file doesn't exist, we treat that as a nil state 141 if !os.IsNotExist(err) { 142 return err 143 } 144 145 // we need a non-nil reader for ReadState and an empty buffer works 146 // to return EOF immediately 147 reader = bytes.NewBuffer(nil) 148 149 } else { 150 defer f.Close() 151 reader = f 152 } 153 } else { 154 // no state to refresh 155 if s.stateFileOut == nil { 156 return nil 157 } 158 159 // we have a state file, make sure we're at the start 160 s.stateFileOut.Seek(0, io.SeekStart) 161 reader = s.stateFileOut 162 } 163 164 state, err := terraform.ReadState(reader) 165 // if there's no state we just assign the nil return value 166 if err != nil && err != terraform.ErrNoState { 167 return err 168 } 169 170 s.state = state 171 s.readState = s.state.DeepCopy() 172 return nil 173 } 174 175 // Lock implements a local filesystem state.Locker. 176 func (s *LocalState) Lock(info *statemgr.LockInfo) (string, error) { 177 s.mu.Lock() 178 defer s.mu.Unlock() 179 180 if s.stateFileOut == nil { 181 if err := s.createStateFiles(); err != nil { 182 return "", err 183 } 184 } 185 186 if s.lockID != "" { 187 return "", fmt.Errorf("state %q already locked", s.stateFileOut.Name()) 188 } 189 190 if err := s.lock(); err != nil { 191 info, infoErr := s.lockInfo() 192 if infoErr != nil { 193 err = multierror.Append(err, infoErr) 194 } 195 196 lockErr := &statemgr.LockError{ 197 Info: info, 198 Err: err, 199 } 200 201 return "", lockErr 202 } 203 204 s.lockID = info.ID 205 return s.lockID, s.writeLockInfo(info) 206 } 207 208 func (s *LocalState) Unlock(id string) error { 209 s.mu.Lock() 210 defer s.mu.Unlock() 211 212 if s.lockID == "" { 213 return fmt.Errorf("LocalState not locked") 214 } 215 216 if id != s.lockID { 217 idErr := fmt.Errorf("invalid lock id: %q. current id: %q", id, s.lockID) 218 info, err := s.lockInfo() 219 if err != nil { 220 idErr = multierror.Append(idErr, err) 221 } 222 223 return &statemgr.LockError{ 224 Err: idErr, 225 Info: info, 226 } 227 } 228 229 os.Remove(s.lockInfoPath()) 230 231 fileName := s.stateFileOut.Name() 232 233 unlockErr := s.unlock() 234 235 s.stateFileOut.Close() 236 s.stateFileOut = nil 237 s.lockID = "" 238 239 // clean up the state file if we created it an never wrote to it 240 stat, err := os.Stat(fileName) 241 if err == nil && stat.Size() == 0 && s.created { 242 os.Remove(fileName) 243 } 244 245 return unlockErr 246 } 247 248 // Open the state file, creating the directories and file as needed. 249 func (s *LocalState) createStateFiles() error { 250 if s.PathOut == "" { 251 s.PathOut = s.Path 252 } 253 254 // yes this could race, but we only use it to clean up empty files 255 if _, err := os.Stat(s.PathOut); os.IsNotExist(err) { 256 s.created = true 257 } 258 259 // Create all the directories 260 if err := os.MkdirAll(filepath.Dir(s.PathOut), 0755); err != nil { 261 return err 262 } 263 264 f, err := os.OpenFile(s.PathOut, os.O_RDWR|os.O_CREATE, 0666) 265 if err != nil { 266 return err 267 } 268 269 s.stateFileOut = f 270 return nil 271 } 272 273 // return the path for the lockInfo metadata. 274 func (s *LocalState) lockInfoPath() string { 275 stateDir, stateName := filepath.Split(s.Path) 276 if stateName == "" { 277 panic("empty state file path") 278 } 279 280 if stateName[0] == '.' { 281 stateName = stateName[1:] 282 } 283 284 return filepath.Join(stateDir, fmt.Sprintf(".%s.lock.info", stateName)) 285 } 286 287 // lockInfo returns the data in a lock info file 288 func (s *LocalState) lockInfo() (*statemgr.LockInfo, error) { 289 path := s.lockInfoPath() 290 infoData, err := ioutil.ReadFile(path) 291 if err != nil { 292 return nil, err 293 } 294 295 info := statemgr.LockInfo{} 296 err = json.Unmarshal(infoData, &info) 297 if err != nil { 298 return nil, fmt.Errorf("state file %q locked, but could not unmarshal lock info: %s", s.Path, err) 299 } 300 return &info, nil 301 } 302 303 // write a new lock info file 304 func (s *LocalState) writeLockInfo(info *statemgr.LockInfo) error { 305 path := s.lockInfoPath() 306 info.Path = s.Path 307 info.Created = time.Now().UTC() 308 309 err := ioutil.WriteFile(path, info.Marshal(), 0600) 310 if err != nil { 311 return fmt.Errorf("could not write lock info for %q: %s", s.Path, err) 312 } 313 return nil 314 }