github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/command/clistate/local_state.go (about)

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