github.com/koding/terraform@v0.6.4-0.20170608090606-5d7e0339779d/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
    52  	s.readState = state
    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
    78  
    79  	if _, err := s.stateFileOut.Seek(0, os.SEEK_SET); err != nil {
    80  		return err
    81  	}
    82  	if err := s.stateFileOut.Truncate(0); err != nil {
    83  		return err
    84  	}
    85  
    86  	if state == nil {
    87  		// if we have no state, don't write anything else.
    88  		return nil
    89  	}
    90  
    91  	s.state.IncrementSerialMaybe(s.readState)
    92  	s.readState = s.state
    93  
    94  	if err := terraform.WriteState(s.state, s.stateFileOut); err != nil {
    95  		return err
    96  	}
    97  
    98  	s.written = true
    99  	return nil
   100  }
   101  
   102  // PersistState for LocalState is a no-op since WriteState always persists.
   103  //
   104  // StatePersister impl.
   105  func (s *LocalState) PersistState() error {
   106  	return nil
   107  }
   108  
   109  // StateRefresher impl.
   110  func (s *LocalState) RefreshState() error {
   111  	s.mu.Lock()
   112  	defer s.mu.Unlock()
   113  
   114  	var reader io.Reader
   115  	if !s.written {
   116  		// we haven't written a state file yet, so load from Path
   117  		f, err := os.Open(s.Path)
   118  		if err != nil {
   119  			// It is okay if the file doesn't exist, we treat that as a nil state
   120  			if !os.IsNotExist(err) {
   121  				return err
   122  			}
   123  
   124  			// we need a non-nil reader for ReadState and an empty buffer works
   125  			// to return EOF immediately
   126  			reader = bytes.NewBuffer(nil)
   127  
   128  		} else {
   129  			defer f.Close()
   130  			reader = f
   131  		}
   132  	} else {
   133  		// no state to refresh
   134  		if s.stateFileOut == nil {
   135  			return nil
   136  		}
   137  
   138  		// we have a state file, make sure we're at the start
   139  		s.stateFileOut.Seek(0, os.SEEK_SET)
   140  		reader = s.stateFileOut
   141  	}
   142  
   143  	state, err := terraform.ReadState(reader)
   144  	// if there's no state we just assign the nil return value
   145  	if err != nil && err != terraform.ErrNoState {
   146  		return err
   147  	}
   148  
   149  	s.state = state
   150  	s.readState = state
   151  	return nil
   152  }
   153  
   154  // Lock implements a local filesystem state.Locker.
   155  func (s *LocalState) Lock(info *LockInfo) (string, error) {
   156  	s.mu.Lock()
   157  	defer s.mu.Unlock()
   158  
   159  	if s.stateFileOut == nil {
   160  		if err := s.createStateFiles(); err != nil {
   161  			return "", err
   162  		}
   163  	}
   164  
   165  	if s.lockID != "" {
   166  		return "", fmt.Errorf("state %q already locked", s.stateFileOut.Name())
   167  	}
   168  
   169  	if err := s.lock(); err != nil {
   170  		info, infoErr := s.lockInfo()
   171  		if infoErr != nil {
   172  			err = multierror.Append(err, infoErr)
   173  		}
   174  
   175  		lockErr := &LockError{
   176  			Info: info,
   177  			Err:  err,
   178  		}
   179  
   180  		return "", lockErr
   181  	}
   182  
   183  	s.lockID = info.ID
   184  	return s.lockID, s.writeLockInfo(info)
   185  }
   186  
   187  func (s *LocalState) Unlock(id string) error {
   188  	s.mu.Lock()
   189  	defer s.mu.Unlock()
   190  
   191  	if s.lockID == "" {
   192  		return fmt.Errorf("LocalState not locked")
   193  	}
   194  
   195  	if id != s.lockID {
   196  		idErr := fmt.Errorf("invalid lock id: %q. current id: %q", id, s.lockID)
   197  		info, err := s.lockInfo()
   198  		if err != nil {
   199  			err = multierror.Append(idErr, err)
   200  		}
   201  
   202  		return &LockError{
   203  			Err:  idErr,
   204  			Info: info,
   205  		}
   206  	}
   207  
   208  	os.Remove(s.lockInfoPath())
   209  
   210  	fileName := s.stateFileOut.Name()
   211  
   212  	unlockErr := s.unlock()
   213  
   214  	s.stateFileOut.Close()
   215  	s.stateFileOut = nil
   216  	s.lockID = ""
   217  
   218  	// clean up the state file if we created it an never wrote to it
   219  	stat, err := os.Stat(fileName)
   220  	if err == nil && stat.Size() == 0 && s.created {
   221  		os.Remove(fileName)
   222  	}
   223  
   224  	return unlockErr
   225  }
   226  
   227  // Open the state file, creating the directories and file as needed.
   228  func (s *LocalState) createStateFiles() error {
   229  	if s.PathOut == "" {
   230  		s.PathOut = s.Path
   231  	}
   232  
   233  	// yes this could race, but we only use it to clean up empty files
   234  	if _, err := os.Stat(s.PathOut); os.IsNotExist(err) {
   235  		s.created = true
   236  	}
   237  
   238  	// Create all the directories
   239  	if err := os.MkdirAll(filepath.Dir(s.PathOut), 0755); err != nil {
   240  		return err
   241  	}
   242  
   243  	f, err := os.OpenFile(s.PathOut, os.O_RDWR|os.O_CREATE, 0666)
   244  	if err != nil {
   245  		return err
   246  	}
   247  
   248  	s.stateFileOut = f
   249  	return nil
   250  }
   251  
   252  // return the path for the lockInfo metadata.
   253  func (s *LocalState) lockInfoPath() string {
   254  	stateDir, stateName := filepath.Split(s.Path)
   255  	if stateName == "" {
   256  		panic("empty state file path")
   257  	}
   258  
   259  	if stateName[0] == '.' {
   260  		stateName = stateName[1:]
   261  	}
   262  
   263  	return filepath.Join(stateDir, fmt.Sprintf(".%s.lock.info", stateName))
   264  }
   265  
   266  // lockInfo returns the data in a lock info file
   267  func (s *LocalState) lockInfo() (*LockInfo, error) {
   268  	path := s.lockInfoPath()
   269  	infoData, err := ioutil.ReadFile(path)
   270  	if err != nil {
   271  		return nil, err
   272  	}
   273  
   274  	info := LockInfo{}
   275  	err = json.Unmarshal(infoData, &info)
   276  	if err != nil {
   277  		return nil, fmt.Errorf("state file %q locked, but could not unmarshal lock info: %s", s.Path, err)
   278  	}
   279  	return &info, nil
   280  }
   281  
   282  // write a new lock info file
   283  func (s *LocalState) writeLockInfo(info *LockInfo) error {
   284  	path := s.lockInfoPath()
   285  	info.Path = s.Path
   286  	info.Created = time.Now().UTC()
   287  
   288  	err := ioutil.WriteFile(path, info.Marshal(), 0600)
   289  	if err != nil {
   290  		return fmt.Errorf("could not write lock info for %q: %s", s.Path, err)
   291  	}
   292  	return nil
   293  }