github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/states/statemgr/filesystem.go (about) 1 package statemgr 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "log" 10 "os" 11 "path/filepath" 12 "sync" 13 "time" 14 15 multierror "github.com/hashicorp/go-multierror" 16 17 "github.com/eliastor/durgaform/internal/states" 18 "github.com/eliastor/durgaform/internal/states/statefile" 19 ) 20 21 // Filesystem is a full state manager that uses a file in the local filesystem 22 // for persistent storage. 23 // 24 // The transient storage for Filesystem is always in-memory. 25 type Filesystem struct { 26 mu sync.Mutex 27 28 // path is the location where a file will be created or replaced for 29 // each persistent snapshot. 30 path string 31 32 // readPath is read by RefreshState instead of "path" until the first 33 // call to PersistState, after which it is ignored. 34 // 35 // The file at readPath must never be written to by this manager. 36 readPath string 37 38 // backupPath is an optional extra path which, if non-empty, will be 39 // created or overwritten with the first state snapshot we read if there 40 // is a subsequent call to write a different state. 41 backupPath string 42 43 // the file handle corresponding to PathOut 44 stateFileOut *os.File 45 46 // While the stateFileOut will correspond to the lock directly, 47 // store and check the lock ID to maintain a strict statemgr.Locker 48 // implementation. 49 lockID string 50 51 // created is set to true if stateFileOut didn't exist before we created it. 52 // This is mostly so we can clean up empty files during tests, but doesn't 53 // hurt to remove file we never wrote to. 54 created bool 55 56 file *statefile.File 57 readFile *statefile.File 58 backupFile *statefile.File 59 writtenBackup bool 60 } 61 62 var ( 63 _ Full = (*Filesystem)(nil) 64 _ PersistentMeta = (*Filesystem)(nil) 65 _ Migrator = (*Filesystem)(nil) 66 ) 67 68 // NewFilesystem creates a filesystem-based state manager that reads and writes 69 // state snapshots at the given filesystem path. 70 // 71 // This is equivalent to calling NewFileSystemBetweenPaths with statePath as 72 // both of the path arguments. 73 func NewFilesystem(statePath string) *Filesystem { 74 return &Filesystem{ 75 path: statePath, 76 readPath: statePath, 77 } 78 } 79 80 // NewFilesystemBetweenPaths creates a filesystem-based state manager that 81 // reads an initial snapshot from readPath and then writes all new snapshots to 82 // writePath. 83 func NewFilesystemBetweenPaths(readPath, writePath string) *Filesystem { 84 return &Filesystem{ 85 path: writePath, 86 readPath: readPath, 87 } 88 } 89 90 // SetBackupPath configures the receiever so that it will create a local 91 // backup file of the next state snapshot it reads (in State) if a different 92 // snapshot is subsequently written (in WriteState). Only one backup is 93 // written for the lifetime of the object, unless reset as described below. 94 // 95 // For correct operation, this must be called before any other state methods 96 // are called. If called multiple times, each call resets the backup 97 // function so that the next read will become the backup snapshot and a 98 // following write will save a backup of it. 99 func (s *Filesystem) SetBackupPath(path string) { 100 s.backupPath = path 101 s.backupFile = nil 102 s.writtenBackup = false 103 } 104 105 // BackupPath returns the manager's backup path if backup files are enabled, 106 // or an empty string otherwise. 107 func (s *Filesystem) BackupPath() string { 108 return s.backupPath 109 } 110 111 // State is an implementation of Reader. 112 func (s *Filesystem) State() *states.State { 113 defer s.mutex()() 114 if s.file == nil { 115 return nil 116 } 117 return s.file.DeepCopy().State 118 } 119 120 // WriteState is an incorrect implementation of Writer that actually also 121 // persists. 122 func (s *Filesystem) WriteState(state *states.State) error { 123 // TODO: this should use a more robust method of writing state, by first 124 // writing to a temp file on the same filesystem, and renaming the file over 125 // the original. 126 127 defer s.mutex()() 128 129 if s.readFile == nil { 130 err := s.refreshState() 131 if err != nil { 132 return err 133 } 134 } 135 136 return s.writeState(state, nil) 137 } 138 139 func (s *Filesystem) writeState(state *states.State, meta *SnapshotMeta) error { 140 if s.stateFileOut == nil { 141 if err := s.createStateFiles(); err != nil { 142 return nil 143 } 144 } 145 defer s.stateFileOut.Sync() 146 147 // We'll try to write our backup first, so we can be sure we've created 148 // it successfully before clobbering the original file it came from. 149 if !s.writtenBackup && s.backupFile != nil && s.backupPath != "" { 150 if !statefile.StatesMarshalEqual(state, s.backupFile.State) { 151 log.Printf("[TRACE] statemgr.Filesystem: creating backup snapshot at %s", s.backupPath) 152 bfh, err := os.Create(s.backupPath) 153 if err != nil { 154 return fmt.Errorf("failed to create local state backup file: %s", err) 155 } 156 defer bfh.Close() 157 158 err = statefile.Write(s.backupFile, bfh) 159 if err != nil { 160 return fmt.Errorf("failed to write to local state backup file: %s", err) 161 } 162 163 s.writtenBackup = true 164 } else { 165 log.Print("[TRACE] statemgr.Filesystem: not making a backup, because the new snapshot is identical to the old") 166 } 167 } else { 168 // This branch is all just logging, to help understand why we didn't make a backup. 169 switch { 170 case s.backupPath == "": 171 log.Print("[TRACE] statemgr.Filesystem: state file backups are disabled") 172 case s.writtenBackup: 173 log.Printf("[TRACE] statemgr.Filesystem: have already backed up original %s to %s on a previous write", s.path, s.backupPath) 174 case s.backupFile == nil: 175 log.Printf("[TRACE] statemgr.Filesystem: no original state snapshot to back up") 176 default: 177 log.Printf("[TRACE] statemgr.Filesystem: not creating a backup for an unknown reason") 178 } 179 } 180 181 s.file = s.file.DeepCopy() 182 if s.file == nil { 183 s.file = NewStateFile() 184 } 185 s.file.State = state.DeepCopy() 186 187 if _, err := s.stateFileOut.Seek(0, io.SeekStart); err != nil { 188 return err 189 } 190 if err := s.stateFileOut.Truncate(0); err != nil { 191 return err 192 } 193 194 if state == nil { 195 // if we have no state, don't write anything else. 196 log.Print("[TRACE] statemgr.Filesystem: state is nil, so leaving the file empty") 197 return nil 198 } 199 200 if meta == nil { 201 if s.readFile == nil || !statefile.StatesMarshalEqual(s.file.State, s.readFile.State) { 202 s.file.Serial++ 203 log.Printf("[TRACE] statemgr.Filesystem: state has changed since last snapshot, so incrementing serial to %d", s.file.Serial) 204 } else { 205 log.Print("[TRACE] statemgr.Filesystem: no state changes since last snapshot") 206 } 207 } else { 208 // Force new metadata 209 s.file.Lineage = meta.Lineage 210 s.file.Serial = meta.Serial 211 log.Printf("[TRACE] statemgr.Filesystem: forcing lineage %q serial %d for migration/import", s.file.Lineage, s.file.Serial) 212 } 213 214 log.Printf("[TRACE] statemgr.Filesystem: writing snapshot at %s", s.path) 215 if err := statefile.Write(s.file, s.stateFileOut); err != nil { 216 return err 217 } 218 219 // Any future reads must come from the file we've now updated 220 s.readPath = s.path 221 return nil 222 } 223 224 // PersistState is an implementation of Persister that does nothing because 225 // this type's Writer implementation does its own persistence. 226 func (s *Filesystem) PersistState() error { 227 return nil 228 } 229 230 // RefreshState is an implementation of Refresher. 231 func (s *Filesystem) RefreshState() error { 232 defer s.mutex()() 233 return s.refreshState() 234 } 235 236 func (s *Filesystem) GetRootOutputValues() (map[string]*states.OutputValue, error) { 237 err := s.RefreshState() 238 if err != nil { 239 return nil, err 240 } 241 242 state := s.State() 243 if state == nil { 244 state = states.NewState() 245 } 246 247 return state.RootModule().OutputValues, nil 248 } 249 250 func (s *Filesystem) refreshState() error { 251 var reader io.Reader 252 253 // The s.readPath file is only OK to read if we have not written any state out 254 // (in which case the same state needs to be read in), and no state output file 255 // has been opened (possibly via a lock) or the input path is different 256 // than the output path. 257 // This is important for Windows, as if the input file is the same as the 258 // output file, and the output file has been locked already, we can't open 259 // the file again. 260 if s.stateFileOut == nil || s.readPath != s.path { 261 // we haven't written a state file yet, so load from readPath 262 log.Printf("[TRACE] statemgr.Filesystem: reading initial snapshot from %s", s.readPath) 263 f, err := os.Open(s.readPath) 264 if err != nil { 265 // It is okay if the file doesn't exist; we'll treat that as a nil state. 266 if !os.IsNotExist(err) { 267 return err 268 } 269 270 // we need a non-nil reader for ReadState and an empty buffer works 271 // to return EOF immediately 272 reader = bytes.NewBuffer(nil) 273 274 } else { 275 defer f.Close() 276 reader = f 277 } 278 } else { 279 log.Printf("[TRACE] statemgr.Filesystem: reading latest snapshot from %s", s.path) 280 // no state to refresh 281 if s.stateFileOut == nil { 282 return nil 283 } 284 285 // we have a state file, make sure we're at the start 286 s.stateFileOut.Seek(0, io.SeekStart) 287 reader = s.stateFileOut 288 } 289 290 f, err := statefile.Read(reader) 291 // if there's no state then a nil file is fine 292 if err != nil { 293 if err != statefile.ErrNoState { 294 return err 295 } 296 log.Printf("[TRACE] statemgr.Filesystem: snapshot file has nil snapshot, but that's okay") 297 } 298 299 s.file = f 300 s.readFile = s.file.DeepCopy() 301 if s.file != nil { 302 log.Printf("[TRACE] statemgr.Filesystem: read snapshot with lineage %q serial %d", s.file.Lineage, s.file.Serial) 303 } else { 304 log.Print("[TRACE] statemgr.Filesystem: read nil snapshot") 305 } 306 return nil 307 } 308 309 // Lock implements Locker using filesystem discretionary locks. 310 func (s *Filesystem) Lock(info *LockInfo) (string, error) { 311 defer s.mutex()() 312 313 if s.stateFileOut == nil { 314 if err := s.createStateFiles(); err != nil { 315 return "", err 316 } 317 } 318 319 if s.lockID != "" { 320 return "", fmt.Errorf("state %q already locked", s.stateFileOut.Name()) 321 } 322 323 if err := s.lock(); err != nil { 324 info, infoErr := s.lockInfo() 325 if infoErr != nil { 326 err = multierror.Append(err, infoErr) 327 } 328 329 lockErr := &LockError{ 330 Info: info, 331 Err: err, 332 } 333 334 return "", lockErr 335 } 336 337 s.lockID = info.ID 338 return s.lockID, s.writeLockInfo(info) 339 } 340 341 // Unlock is the companion to Lock, completing the implemention of Locker. 342 func (s *Filesystem) Unlock(id string) error { 343 defer s.mutex()() 344 345 if s.lockID == "" { 346 return fmt.Errorf("LocalState not locked") 347 } 348 349 if id != s.lockID { 350 idErr := fmt.Errorf("invalid lock id: %q. current id: %q", id, s.lockID) 351 info, err := s.lockInfo() 352 if err != nil { 353 idErr = multierror.Append(idErr, err) 354 } 355 356 return &LockError{ 357 Err: idErr, 358 Info: info, 359 } 360 } 361 362 lockInfoPath := s.lockInfoPath() 363 log.Printf("[TRACE] statemgr.Filesystem: removing lock metadata file %s", lockInfoPath) 364 os.Remove(lockInfoPath) 365 366 fileName := s.stateFileOut.Name() 367 368 unlockErr := s.unlock() 369 370 s.stateFileOut.Close() 371 s.stateFileOut = nil 372 s.lockID = "" 373 374 // clean up the state file if we created it an never wrote to it 375 stat, err := os.Stat(fileName) 376 if err == nil && stat.Size() == 0 && s.created { 377 os.Remove(fileName) 378 } 379 380 return unlockErr 381 } 382 383 // StateSnapshotMeta returns the metadata from the most recently persisted 384 // or refreshed persistent state snapshot. 385 // 386 // This is an implementation of PersistentMeta. 387 func (s *Filesystem) StateSnapshotMeta() SnapshotMeta { 388 if s.file == nil { 389 return SnapshotMeta{} // placeholder 390 } 391 392 return SnapshotMeta{ 393 Lineage: s.file.Lineage, 394 Serial: s.file.Serial, 395 396 DurgaformVersion: s.file.DurgaformVersion, 397 } 398 } 399 400 // StateForMigration is part of our implementation of Migrator. 401 func (s *Filesystem) StateForMigration() *statefile.File { 402 return s.file.DeepCopy() 403 } 404 405 // WriteStateForMigration is part of our implementation of Migrator. 406 func (s *Filesystem) WriteStateForMigration(f *statefile.File, force bool) error { 407 defer s.mutex()() 408 409 if s.readFile == nil { 410 err := s.refreshState() 411 if err != nil { 412 return err 413 } 414 } 415 416 if !force { 417 err := CheckValidImport(f, s.readFile) 418 if err != nil { 419 return err 420 } 421 } 422 423 if s.readFile != nil { 424 log.Printf( 425 "[TRACE] statemgr.Filesystem: Importing snapshot with lineage %q serial %d over snapshot with lineage %q serial %d at %s", 426 f.Lineage, f.Serial, 427 s.readFile.Lineage, s.readFile.Serial, 428 s.path, 429 ) 430 } else { 431 log.Printf( 432 "[TRACE] statemgr.Filesystem: Importing snapshot with lineage %q serial %d as the initial state snapshot at %s", 433 f.Lineage, f.Serial, 434 s.path, 435 ) 436 } 437 438 err := s.writeState(f.State, &SnapshotMeta{Lineage: f.Lineage, Serial: f.Serial}) 439 if err != nil { 440 return err 441 } 442 443 return nil 444 } 445 446 // Open the state file, creating the directories and file as needed. 447 func (s *Filesystem) createStateFiles() error { 448 log.Printf("[TRACE] statemgr.Filesystem: preparing to manage state snapshots at %s", s.path) 449 450 // This could race, but we only use it to clean up empty files 451 if _, err := os.Stat(s.path); os.IsNotExist(err) { 452 s.created = true 453 } 454 455 // Create all the directories 456 if err := os.MkdirAll(filepath.Dir(s.path), 0755); err != nil { 457 return err 458 } 459 460 f, err := os.OpenFile(s.path, os.O_RDWR|os.O_CREATE, 0666) 461 if err != nil { 462 return err 463 } 464 465 s.stateFileOut = f 466 467 // If the file already existed with content then that'll be the content 468 // of our backup file if we write a change later. 469 s.backupFile, err = statefile.Read(s.stateFileOut) 470 if err != nil { 471 if err != statefile.ErrNoState { 472 return err 473 } 474 log.Printf("[TRACE] statemgr.Filesystem: no previously-stored snapshot exists") 475 } else { 476 log.Printf("[TRACE] statemgr.Filesystem: existing snapshot has lineage %q serial %d", s.backupFile.Lineage, s.backupFile.Serial) 477 } 478 479 // Refresh now, to load in the snapshot if the file already existed 480 return nil 481 } 482 483 // return the path for the lockInfo metadata. 484 func (s *Filesystem) lockInfoPath() string { 485 stateDir, stateName := filepath.Split(s.path) 486 if stateName == "" { 487 panic("empty state file path") 488 } 489 490 if stateName[0] == '.' { 491 stateName = stateName[1:] 492 } 493 494 return filepath.Join(stateDir, fmt.Sprintf(".%s.lock.info", stateName)) 495 } 496 497 // lockInfo returns the data in a lock info file 498 func (s *Filesystem) lockInfo() (*LockInfo, error) { 499 path := s.lockInfoPath() 500 infoData, err := ioutil.ReadFile(path) 501 if err != nil { 502 return nil, err 503 } 504 505 info := LockInfo{} 506 err = json.Unmarshal(infoData, &info) 507 if err != nil { 508 return nil, fmt.Errorf("state file %q locked, but could not unmarshal lock info: %s", s.readPath, err) 509 } 510 return &info, nil 511 } 512 513 // write a new lock info file 514 func (s *Filesystem) writeLockInfo(info *LockInfo) error { 515 path := s.lockInfoPath() 516 info.Path = s.readPath 517 info.Created = time.Now().UTC() 518 519 log.Printf("[TRACE] statemgr.Filesystem: writing lock metadata to %s", path) 520 err := ioutil.WriteFile(path, info.Marshal(), 0600) 521 if err != nil { 522 return fmt.Errorf("could not write lock info for %q: %s", s.readPath, err) 523 } 524 return nil 525 } 526 527 func (s *Filesystem) mutex() func() { 528 s.mu.Lock() 529 return s.mu.Unlock 530 }