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