// Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2023 HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package statemgr import ( "bytes" "encoding/json" "fmt" "io" "log" "os" "path/filepath" "sync" "time" multierror "github.com/hashicorp/go-multierror" "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/states" "github.com/opentofu/opentofu/internal/states/statefile" "github.com/opentofu/opentofu/internal/tofu" ) // Filesystem is a full state manager that uses a file in the local filesystem // for persistent storage. // // The transient storage for Filesystem is always in-memory. type Filesystem struct { // path is the location where a file will be created or replaced for // each persistent snapshot. path string // readPath is read by RefreshState instead of "path" until the first // call to PersistState, after which it is ignored. // // The file at readPath must never be written to by this manager. readPath string // backupPath is an optional extra path which, if non-empty, will be // created or overwritten with the first state snapshot we read if there // is a subsequent call to write a different state. backupPath string // the file handle corresponding to PathOut stateFileOut *os.File // While the stateFileOut will correspond to the lock directly, // store and check the lock ID to maintain a strict statemgr.Locker // implementation. lockID string // created is set to true if stateFileOut didn't exist before we created it. // This is mostly so we can clean up empty files during tests, but doesn't // hurt to remove file we never wrote to. created bool mu sync.Mutex file, readFile *statefile.File backupFile *statefile.File writtenBackup bool encryption encryption.StateEncryption } var ( _ Full = (*Filesystem)(nil) _ PersistentMeta = (*Filesystem)(nil) _ Migrator = (*Filesystem)(nil) ) // NewFilesystem creates a filesystem-based state manager that reads and writes // state snapshots at the given filesystem path. // // This is equivalent to calling NewFileSystemBetweenPaths with statePath as // both of the path arguments. func NewFilesystem(statePath string, enc encryption.StateEncryption) *Filesystem { return &Filesystem{ path: statePath, readPath: statePath, encryption: enc, } } // NewFilesystemBetweenPaths creates a filesystem-based state manager that // reads an initial snapshot from readPath and then writes all new snapshots to // writePath. func NewFilesystemBetweenPaths(readPath, writePath string, enc encryption.StateEncryption) *Filesystem { return &Filesystem{ path: writePath, readPath: readPath, encryption: enc, } } // SetBackupPath configures the receiever so that it will create a local // backup file of the next state snapshot it reads (in State) if a different // snapshot is subsequently written (in WriteState). Only one backup is // written for the lifetime of the object, unless reset as described below. // // For correct operation, this must be called before any other state methods // are called. If called multiple times, each call resets the backup // function so that the next read will become the backup snapshot and a // following write will save a backup of it. func (s *Filesystem) SetBackupPath(path string) { s.backupPath = path s.backupFile = nil s.writtenBackup = false } // BackupPath returns the manager's backup path if backup files are enabled, // or an empty string otherwise. func (s *Filesystem) BackupPath() string { return s.backupPath } // State is an implementation of Reader. func (s *Filesystem) State() *states.State { defer s.mutex()() if s.file == nil { return nil } return s.file.DeepCopy().State } // WriteState is an implementation of Writer. func (s *Filesystem) WriteState(state *states.State) error { defer s.mutex()() if s.readFile == nil { err := s.refreshState() if err != nil { return err } } return s.writeState(state, nil) } func (s *Filesystem) writeState(state *states.State, meta *SnapshotMeta) error { s.file = s.file.DeepCopy() if s.file == nil { s.file = NewStateFile() } s.file.State = state.DeepCopy() if meta != nil { // Force new metadata s.file.Lineage = meta.Lineage s.file.Serial = meta.Serial log.Printf("[TRACE] statemgr.Filesystem: forcing lineage %q serial %d for migration/import", s.file.Lineage, s.file.Serial) } return nil } // PersistState writes state to a tfstate file. func (s *Filesystem) PersistState(schemas *tofu.Schemas) error { defer s.mutex()() return s.persistState(schemas) } func (s *Filesystem) persistState(schemas *tofu.Schemas) error { // TODO: this should use a more robust method of writing state, by first // writing to a temp file on the same filesystem, and renaming the file over // the original. if s.stateFileOut == nil { if err := s.createStateFiles(); err != nil { return nil } } defer s.stateFileOut.Sync() if s.file == nil { s.file = NewStateFile() } state := s.file.State // We'll try to write our backup first, so we can be sure we've created // it successfully before clobbering the original file it came from. if !s.writtenBackup && s.backupFile != nil && s.backupPath != "" { if !statefile.StatesMarshalEqual(state, s.backupFile.State) { log.Printf("[TRACE] statemgr.Filesystem: creating backup snapshot at %s", s.backupPath) bfh, err := os.Create(s.backupPath) if err != nil { return fmt.Errorf("failed to create local state backup file: %w", err) } defer bfh.Close() err = statefile.Write(s.backupFile, bfh, s.encryption) if err != nil { return fmt.Errorf("failed to write to local state backup file: %w", err) } s.writtenBackup = true } else { log.Print("[TRACE] statemgr.Filesystem: not making a backup, because the new snapshot is identical to the old") } } else { // This branch is all just logging, to help understand why we didn't make a backup. switch { case s.backupPath == "": log.Print("[TRACE] statemgr.Filesystem: state file backups are disabled") case s.writtenBackup: log.Printf("[TRACE] statemgr.Filesystem: have already backed up original %s to %s on a previous write", s.path, s.backupPath) case s.backupFile == nil: log.Printf("[TRACE] statemgr.Filesystem: no original state snapshot to back up") default: log.Printf("[TRACE] statemgr.Filesystem: not creating a backup for an unknown reason") } } if _, err := s.stateFileOut.Seek(0, io.SeekStart); err != nil { return err } if err := s.stateFileOut.Truncate(0); err != nil { return err } if state == nil { // if we have no state, don't write anything else. log.Print("[TRACE] statemgr.Filesystem: state is nil, so leaving the file empty") return nil } if s.readFile == nil || !statefile.StatesMarshalEqual(s.file.State, s.readFile.State) { s.file.Serial++ log.Printf("[TRACE] statemgr.Filesystem: state has changed since last snapshot, so incrementing serial to %d", s.file.Serial) } else { log.Print("[TRACE] statemgr.Filesystem: no state changes since last snapshot") } log.Printf("[TRACE] statemgr.Filesystem: writing snapshot at %s", s.path) if err := statefile.Write(s.file, s.stateFileOut, s.encryption); err != nil { return err } // Any future reads must come from the file we've now updated s.readPath = s.path return nil } // RefreshState is an implementation of Refresher. func (s *Filesystem) RefreshState() error { defer s.mutex()() return s.refreshState() } func (s *Filesystem) GetRootOutputValues() (map[string]*states.OutputValue, error) { err := s.RefreshState() if err != nil { return nil, err } state := s.State() if state == nil { state = states.NewState() } return state.RootModule().OutputValues, nil } func (s *Filesystem) refreshState() error { var reader io.Reader // The s.readPath file is only OK to read if we have not written any state out // (in which case the same state needs to be read in), and no state output file // has been opened (possibly via a lock) or the input path is different // than the output path. // This is important for Windows, as if the input file is the same as the // output file, and the output file has been locked already, we can't open // the file again. if s.stateFileOut == nil || s.readPath != s.path { // we haven't written a state file yet, so load from readPath log.Printf("[TRACE] statemgr.Filesystem: reading initial snapshot from %s", s.readPath) f, err := os.Open(s.readPath) if err != nil { // It is okay if the file doesn't exist; we'll treat that as a nil state. if !os.IsNotExist(err) { return err } // we need a non-nil reader for ReadState and an empty buffer works // to return EOF immediately reader = bytes.NewBuffer(nil) } else { defer f.Close() reader = f } } else { log.Printf("[TRACE] statemgr.Filesystem: reading latest snapshot from %s", s.path) // no state to refresh if s.stateFileOut == nil { return nil } // we have a state file, make sure we're at the start _, err := s.stateFileOut.Seek(0, io.SeekStart) if err != nil { return err } reader = s.stateFileOut } f, err := statefile.Read(reader, s.encryption) // if there's no state then a nil file is fine if err != nil { if err != statefile.ErrNoState { return err } log.Printf("[TRACE] statemgr.Filesystem: snapshot file has nil snapshot, but that's okay") } s.file = f s.readFile = s.file.DeepCopy() if s.file != nil { log.Printf("[TRACE] statemgr.Filesystem: read snapshot with lineage %q serial %d", s.file.Lineage, s.file.Serial) } else { log.Print("[TRACE] statemgr.Filesystem: read nil snapshot") } return nil } // Lock implements Locker using filesystem discretionary locks. func (s *Filesystem) Lock(info *LockInfo) (string, error) { defer s.mutex()() if s.stateFileOut == nil { if err := s.createStateFiles(); err != nil { return "", err } } if s.lockID != "" { return "", fmt.Errorf("state %q already locked", s.stateFileOut.Name()) } if err := s.lock(); err != nil { info, infoErr := s.lockInfo() if infoErr != nil { err = multierror.Append(err, infoErr) } lockErr := &LockError{ Info: info, Err: err, } return "", lockErr } s.lockID = info.ID return s.lockID, s.writeLockInfo(info) } // Unlock is the companion to Lock, completing the implemention of Locker. func (s *Filesystem) Unlock(id string) error { defer s.mutex()() if s.lockID == "" { return fmt.Errorf("LocalState not locked") } if id != s.lockID { idErr := fmt.Errorf("invalid lock id: %q. current id: %q", id, s.lockID) info, err := s.lockInfo() if err != nil { idErr = multierror.Append(idErr, err) } return &LockError{ Err: idErr, Info: info, } } lockInfoPath := s.lockInfoPath() err := os.Remove(lockInfoPath) if err != nil { log.Printf( "[ERROR] statemgr.Filesystem: error removing lock metadata file %q: %s", lockInfoPath, err, ) } else { log.Printf("[TRACE] statemgr.Filesystem: removed lock metadata file %s", lockInfoPath) } fileName := s.stateFileOut.Name() unlockErr := s.unlock() s.stateFileOut.Close() s.stateFileOut = nil s.lockID = "" // clean up the state file if we created it an never wrote to it stat, err := os.Stat(fileName) if err == nil && stat.Size() == 0 && s.created { err = os.Remove(fileName) if err != nil { log.Printf("[ERROR] stagemgr.Filesystem: error removing empty state file %q: %s", fileName, err) } } return unlockErr } // StateSnapshotMeta returns the metadata from the most recently persisted // or refreshed persistent state snapshot. // // This is an implementation of PersistentMeta. func (s *Filesystem) StateSnapshotMeta() SnapshotMeta { if s.file == nil { return SnapshotMeta{} // placeholder } return SnapshotMeta{ Lineage: s.file.Lineage, Serial: s.file.Serial, TerraformVersion: s.file.TerraformVersion, } } // StateForMigration is part of our implementation of Migrator. func (s *Filesystem) StateForMigration() *statefile.File { return s.file.DeepCopy() } // WriteStateForMigration is part of our implementation of Migrator. func (s *Filesystem) WriteStateForMigration(f *statefile.File, force bool) error { defer s.mutex()() if s.readFile == nil { err := s.refreshState() if err != nil { return err } } if !force { err := CheckValidImport(f, s.readFile) if err != nil { return err } } if s.readFile != nil { log.Printf( "[TRACE] statemgr.Filesystem: Importing snapshot with lineage %q serial %d over snapshot with lineage %q serial %d at %s", f.Lineage, f.Serial, s.readFile.Lineage, s.readFile.Serial, s.path, ) } else { log.Printf( "[TRACE] statemgr.Filesystem: Importing snapshot with lineage %q serial %d as the initial state snapshot at %s", f.Lineage, f.Serial, s.path, ) } err := s.writeState(f.State, &SnapshotMeta{Lineage: f.Lineage, Serial: f.Serial}) if err != nil { return err } return s.persistState(nil) } // Open the state file, creating the directories and file as needed. func (s *Filesystem) createStateFiles() error { log.Printf("[TRACE] statemgr.Filesystem: preparing to manage state snapshots at %s", s.path) // This could race, but we only use it to clean up empty files if _, err := os.Stat(s.path); os.IsNotExist(err) { s.created = true } // Create all the directories if err := os.MkdirAll(filepath.Dir(s.path), 0755); err != nil { return err } f, err := os.OpenFile(s.path, os.O_RDWR|os.O_CREATE, 0666) if err != nil { return err } s.stateFileOut = f // If the file already existed with content then that'll be the content // of our backup file if we write a change later. s.backupFile, err = statefile.Read(s.stateFileOut, s.encryption) if err != nil { if err != statefile.ErrNoState { return err } log.Printf("[TRACE] statemgr.Filesystem: no previously-stored snapshot exists") } else { log.Printf("[TRACE] statemgr.Filesystem: existing snapshot has lineage %q serial %d", s.backupFile.Lineage, s.backupFile.Serial) } // Refresh now, to load in the snapshot if the file already existed return nil } // return the path for the lockInfo metadata. func (s *Filesystem) lockInfoPath() string { stateDir, stateName := filepath.Split(s.path) if stateName == "" { panic("empty state file path") } if stateName[0] == '.' { stateName = stateName[1:] } return filepath.Join(stateDir, fmt.Sprintf(".%s.lock.info", stateName)) } // lockInfo returns the data in a lock info file func (s *Filesystem) lockInfo() (*LockInfo, error) { path := s.lockInfoPath() infoData, err := os.ReadFile(path) if err != nil { return nil, err } info := LockInfo{} err = json.Unmarshal(infoData, &info) if err != nil { return nil, fmt.Errorf("state file %q locked, but could not unmarshal lock info: %w", s.readPath, err) } return &info, nil } // write a new lock info file func (s *Filesystem) writeLockInfo(info *LockInfo) error { path := s.lockInfoPath() info.Path = s.readPath info.Created = time.Now().UTC() log.Printf("[TRACE] statemgr.Filesystem: writing lock metadata to %s", path) err := os.WriteFile(path, info.Marshal(), 0600) if err != nil { return fmt.Errorf("could not write lock info for %q: %w", s.readPath, err) } return nil } func (s *Filesystem) mutex() func() { s.mu.Lock() return s.mu.Unlock }