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