github.com/idansluprisma/atesta@v0.11.12-beta1/state/local.go (about)

     1  package state
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  	"path/filepath"
    11  	"sync"
    12  	"time"
    13  
    14  	multierror "github.com/hashicorp/go-multierror"
    15  	"github.com/hashicorp/terraform/terraform"
    16  )
    17  
    18  // LocalState manages a state storage that is local to the filesystem.
    19  type LocalState struct {
    20  	mu sync.Mutex
    21  
    22  	// Path is the path to read the state from. PathOut is the path to
    23  	// write the state to. If PathOut is not specified, Path will be used.
    24  	// If PathOut already exists, it will be overwritten.
    25  	Path    string
    26  	PathOut string
    27  
    28  	// the file handle corresponding to PathOut
    29  	stateFileOut *os.File
    30  
    31  	// While the stateFileOut will correspond to the lock directly,
    32  	// store and check the lock ID to maintain a strict state.Locker
    33  	// implementation.
    34  	lockID string
    35  
    36  	// created is set to true if stateFileOut didn't exist before we created it.
    37  	// This is mostly so we can clean up emtpy files during tests, but doesn't
    38  	// hurt to remove file we never wrote to.
    39  	created bool
    40  
    41  	state     *terraform.State
    42  	readState *terraform.State
    43  	written   bool
    44  }
    45  
    46  // SetState will force a specific state in-memory for this local state.
    47  func (s *LocalState) SetState(state *terraform.State) {
    48  	s.mu.Lock()
    49  	defer s.mu.Unlock()
    50  
    51  	s.state = state.DeepCopy()
    52  	s.readState = state.DeepCopy()
    53  }
    54  
    55  // StateReader impl.
    56  func (s *LocalState) State() *terraform.State {
    57  	return s.state.DeepCopy()
    58  }
    59  
    60  // WriteState for LocalState always persists the state as well.
    61  // TODO: this should use a more robust method of writing state, by first
    62  // writing to a temp file on the same filesystem, and renaming the file over
    63  // the original.
    64  //
    65  // StateWriter impl.
    66  func (s *LocalState) WriteState(state *terraform.State) error {
    67  	s.mu.Lock()
    68  	defer s.mu.Unlock()
    69  
    70  	if s.stateFileOut == nil {
    71  		if err := s.createStateFiles(); err != nil {
    72  			return nil
    73  		}
    74  	}
    75  	defer s.stateFileOut.Sync()
    76  
    77  	s.state = state.DeepCopy() // don't want mutations before we actually get this written to disk
    78  
    79  	if s.readState != nil && s.state != nil {
    80  		// We don't trust callers to properly manage serials. Instead, we assume
    81  		// that a WriteState is always for the next version after what was
    82  		// most recently read.
    83  		s.state.Serial = s.readState.Serial
    84  	}
    85  
    86  	if _, err := s.stateFileOut.Seek(0, io.SeekStart); err != nil {
    87  		return err
    88  	}
    89  	if err := s.stateFileOut.Truncate(0); err != nil {
    90  		return err
    91  	}
    92  
    93  	if state == nil {
    94  		// if we have no state, don't write anything else.
    95  		return nil
    96  	}
    97  
    98  	if !s.state.MarshalEqual(s.readState) {
    99  		s.state.Serial++
   100  	}
   101  
   102  	if err := terraform.WriteState(s.state, s.stateFileOut); err != nil {
   103  		return err
   104  	}
   105  
   106  	s.written = true
   107  	return nil
   108  }
   109  
   110  // PersistState for LocalState is a no-op since WriteState always persists.
   111  //
   112  // StatePersister impl.
   113  func (s *LocalState) PersistState() error {
   114  	return nil
   115  }
   116  
   117  // StateRefresher impl.
   118  func (s *LocalState) RefreshState() error {
   119  	s.mu.Lock()
   120  	defer s.mu.Unlock()
   121  
   122  	if s.PathOut == "" {
   123  		s.PathOut = s.Path
   124  	}
   125  
   126  	var reader io.Reader
   127  
   128  	// The s.Path file is only OK to read if we have not written any state out
   129  	// (in which case the same state needs to be read in), and no state output file
   130  	// has been opened (possibly via a lock) or the input path is different
   131  	// than the output path.
   132  	// This is important for Windows, as if the input file is the same as the
   133  	// output file, and the output file has been locked already, we can't open
   134  	// the file again.
   135  	if !s.written && (s.stateFileOut == nil || s.Path != s.PathOut) {
   136  		// we haven't written a state file yet, so load from Path
   137  		f, err := os.Open(s.Path)
   138  		if err != nil {
   139  			// It is okay if the file doesn't exist, we treat that as a nil state
   140  			if !os.IsNotExist(err) {
   141  				return err
   142  			}
   143  
   144  			// we need a non-nil reader for ReadState and an empty buffer works
   145  			// to return EOF immediately
   146  			reader = bytes.NewBuffer(nil)
   147  
   148  		} else {
   149  			defer f.Close()
   150  			reader = f
   151  		}
   152  	} else {
   153  		// no state to refresh
   154  		if s.stateFileOut == nil {
   155  			return nil
   156  		}
   157  
   158  		// we have a state file, make sure we're at the start
   159  		s.stateFileOut.Seek(0, io.SeekStart)
   160  		reader = s.stateFileOut
   161  	}
   162  
   163  	state, err := terraform.ReadState(reader)
   164  	// if there's no state we just assign the nil return value
   165  	if err != nil && err != terraform.ErrNoState {
   166  		return err
   167  	}
   168  
   169  	s.state = state
   170  	s.readState = s.state.DeepCopy()
   171  	return nil
   172  }
   173  
   174  // Lock implements a local filesystem state.Locker.
   175  func (s *LocalState) Lock(info *LockInfo) (string, error) {
   176  	s.mu.Lock()
   177  	defer s.mu.Unlock()
   178  
   179  	if s.stateFileOut == nil {
   180  		if err := s.createStateFiles(); err != nil {
   181  			return "", err
   182  		}
   183  	}
   184  
   185  	if s.lockID != "" {
   186  		return "", fmt.Errorf("state %q already locked", s.stateFileOut.Name())
   187  	}
   188  
   189  	if err := s.lock(); err != nil {
   190  		info, infoErr := s.lockInfo()
   191  		if infoErr != nil {
   192  			err = multierror.Append(err, infoErr)
   193  		}
   194  
   195  		lockErr := &LockError{
   196  			Info: info,
   197  			Err:  err,
   198  		}
   199  
   200  		return "", lockErr
   201  	}
   202  
   203  	s.lockID = info.ID
   204  	return s.lockID, s.writeLockInfo(info)
   205  }
   206  
   207  func (s *LocalState) Unlock(id string) error {
   208  	s.mu.Lock()
   209  	defer s.mu.Unlock()
   210  
   211  	if s.lockID == "" {
   212  		return fmt.Errorf("LocalState not locked")
   213  	}
   214  
   215  	if id != s.lockID {
   216  		idErr := fmt.Errorf("invalid lock id: %q. current id: %q", id, s.lockID)
   217  		info, err := s.lockInfo()
   218  		if err != nil {
   219  			err = multierror.Append(idErr, err)
   220  		}
   221  
   222  		return &LockError{
   223  			Err:  idErr,
   224  			Info: info,
   225  		}
   226  	}
   227  
   228  	os.Remove(s.lockInfoPath())
   229  
   230  	fileName := s.stateFileOut.Name()
   231  
   232  	unlockErr := s.unlock()
   233  
   234  	s.stateFileOut.Close()
   235  	s.stateFileOut = nil
   236  	s.lockID = ""
   237  
   238  	// clean up the state file if we created it an never wrote to it
   239  	stat, err := os.Stat(fileName)
   240  	if err == nil && stat.Size() == 0 && s.created {
   241  		os.Remove(fileName)
   242  	}
   243  
   244  	return unlockErr
   245  }
   246  
   247  // Open the state file, creating the directories and file as needed.
   248  func (s *LocalState) createStateFiles() error {
   249  	if s.PathOut == "" {
   250  		s.PathOut = s.Path
   251  	}
   252  
   253  	// yes this could race, but we only use it to clean up empty files
   254  	if _, err := os.Stat(s.PathOut); os.IsNotExist(err) {
   255  		s.created = true
   256  	}
   257  
   258  	// Create all the directories
   259  	if err := os.MkdirAll(filepath.Dir(s.PathOut), 0755); err != nil {
   260  		return err
   261  	}
   262  
   263  	f, err := os.OpenFile(s.PathOut, os.O_RDWR|os.O_CREATE, 0666)
   264  	if err != nil {
   265  		return err
   266  	}
   267  
   268  	s.stateFileOut = f
   269  	return nil
   270  }
   271  
   272  // return the path for the lockInfo metadata.
   273  func (s *LocalState) lockInfoPath() string {
   274  	stateDir, stateName := filepath.Split(s.Path)
   275  	if stateName == "" {
   276  		panic("empty state file path")
   277  	}
   278  
   279  	if stateName[0] == '.' {
   280  		stateName = stateName[1:]
   281  	}
   282  
   283  	return filepath.Join(stateDir, fmt.Sprintf(".%s.lock.info", stateName))
   284  }
   285  
   286  // lockInfo returns the data in a lock info file
   287  func (s *LocalState) lockInfo() (*LockInfo, error) {
   288  	path := s.lockInfoPath()
   289  	infoData, err := ioutil.ReadFile(path)
   290  	if err != nil {
   291  		return nil, err
   292  	}
   293  
   294  	info := LockInfo{}
   295  	err = json.Unmarshal(infoData, &info)
   296  	if err != nil {
   297  		return nil, fmt.Errorf("state file %q locked, but could not unmarshal lock info: %s", s.Path, err)
   298  	}
   299  	return &info, nil
   300  }
   301  
   302  // write a new lock info file
   303  func (s *LocalState) writeLockInfo(info *LockInfo) error {
   304  	path := s.lockInfoPath()
   305  	info.Path = s.Path
   306  	info.Created = time.Now().UTC()
   307  
   308  	err := ioutil.WriteFile(path, info.Marshal(), 0600)
   309  	if err != nil {
   310  		return fmt.Errorf("could not write lock info for %q: %s", s.Path, err)
   311  	}
   312  	return nil
   313  }