github.com/opentofu/opentofu@v1.7.1/internal/states/statemgr/filesystem.go (about)

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