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