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  }