github.com/chalford/terraform@v0.3.7-0.20150113080010-a78c69a8c81f/remote/remote.go (about)

     1  package remote
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/md5"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  	"path/filepath"
    11  
    12  	"github.com/hashicorp/terraform/terraform"
    13  )
    14  
    15  const (
    16  	// LocalDirectory is the directory created in the working
    17  	// dir to hold the remote state file.
    18  	LocalDirectory = ".terraform"
    19  
    20  	// HiddenStateFile is the name of the state file in the
    21  	// LocalDirectory
    22  	HiddenStateFile = "terraform.tfstate"
    23  
    24  	// BackupHiddenStateFile is the path we backup the state
    25  	// file to before modifications are made
    26  	BackupHiddenStateFile = "terraform.tfstate.backup"
    27  )
    28  
    29  // StateChangeResult is used to communicate to a caller
    30  // what actions have been taken when updating a state file
    31  type StateChangeResult int
    32  
    33  const (
    34  	// StateChangeNoop indicates nothing has happened,
    35  	// but that does not indicate an error. Everything is
    36  	// just up to date. (Push/Pull)
    37  	StateChangeNoop StateChangeResult = iota
    38  
    39  	// StateChangeInit indicates that there is no local or
    40  	// remote state, and that the state was initialized
    41  	StateChangeInit
    42  
    43  	// StateChangeUpdateLocal indicates the local state
    44  	// was updated. (Pull)
    45  	StateChangeUpdateLocal
    46  
    47  	// StateChangeUpdateRemote indicates the remote state
    48  	// was updated. (Push)
    49  	StateChangeUpdateRemote
    50  
    51  	// StateChangeLocalNewer means the pull was a no-op
    52  	// because the local state is newer than that of the
    53  	// server. This means a Push should take place. (Pull)
    54  	StateChangeLocalNewer
    55  
    56  	// StateChangeRemoteNewer means the push was a no-op
    57  	// because the remote state is newer than that of the
    58  	// local state. This means a Pull should take place.
    59  	// (Push)
    60  	StateChangeRemoteNewer
    61  
    62  	// StateChangeConflict means that the push or pull
    63  	// was a no-op because there is a conflict. This means
    64  	// there are multiple state definitions at the same
    65  	// serial number with different contents. This requires
    66  	// an operator to intervene and resolve the conflict.
    67  	// Shame on the user for doing concurrent apply.
    68  	// (Push/Pull)
    69  	StateChangeConflict
    70  )
    71  
    72  func (sc StateChangeResult) String() string {
    73  	switch sc {
    74  	case StateChangeNoop:
    75  		return "Local and remote state in sync"
    76  	case StateChangeInit:
    77  		return "Local state initialized"
    78  	case StateChangeUpdateLocal:
    79  		return "Local state updated"
    80  	case StateChangeUpdateRemote:
    81  		return "Remote state updated"
    82  	case StateChangeLocalNewer:
    83  		return "Local state is newer than remote state, push required"
    84  	case StateChangeRemoteNewer:
    85  		return "Remote state is newer than local state, pull required"
    86  	case StateChangeConflict:
    87  		return "Local and remote state conflict, manual resolution required"
    88  	default:
    89  		return fmt.Sprintf("Unknown state change type: %d", sc)
    90  	}
    91  }
    92  
    93  // SuccessfulPull is used to clasify the StateChangeResult for
    94  // a pull operation. This is different by operation, but can be used
    95  // to determine a proper exit code.
    96  func (sc StateChangeResult) SuccessfulPull() bool {
    97  	switch sc {
    98  	case StateChangeNoop:
    99  		return true
   100  	case StateChangeInit:
   101  		return true
   102  	case StateChangeUpdateLocal:
   103  		return true
   104  	case StateChangeLocalNewer:
   105  		return false
   106  	case StateChangeConflict:
   107  		return false
   108  	default:
   109  		return false
   110  	}
   111  }
   112  
   113  // SuccessfulPush is used to clasify the StateChangeResult for
   114  // a push operation. This is different by operation, but can be used
   115  // to determine a proper exit code
   116  func (sc StateChangeResult) SuccessfulPush() bool {
   117  	switch sc {
   118  	case StateChangeNoop:
   119  		return true
   120  	case StateChangeUpdateRemote:
   121  		return true
   122  	case StateChangeRemoteNewer:
   123  		return false
   124  	case StateChangeConflict:
   125  		return false
   126  	default:
   127  		return false
   128  	}
   129  }
   130  
   131  // EnsureDirectory is used to make sure the local storage
   132  // directory exists
   133  func EnsureDirectory() error {
   134  	cwd, err := os.Getwd()
   135  	if err != nil {
   136  		return fmt.Errorf("Failed to get current directory: %v", err)
   137  	}
   138  	path := filepath.Join(cwd, LocalDirectory)
   139  	if err := os.Mkdir(path, 0770); err != nil {
   140  		if os.IsExist(err) {
   141  			return nil
   142  		}
   143  		return fmt.Errorf("Failed to make directory '%s': %v", path, err)
   144  	}
   145  	return nil
   146  }
   147  
   148  // HiddenStatePath is used to return the path to the hidden state file,
   149  // should there be one.
   150  // TODO: Rename to LocalStatePath
   151  func HiddenStatePath() (string, error) {
   152  	cwd, err := os.Getwd()
   153  	if err != nil {
   154  		return "", fmt.Errorf("Failed to get current directory: %v", err)
   155  	}
   156  	path := filepath.Join(cwd, LocalDirectory, HiddenStateFile)
   157  	return path, nil
   158  }
   159  
   160  // HaveLocalState is used to check if we have a local state file
   161  func HaveLocalState() (bool, error) {
   162  	path, err := HiddenStatePath()
   163  	if err != nil {
   164  		return false, err
   165  	}
   166  	return ExistsFile(path)
   167  }
   168  
   169  // ExistsFile is used to check if a given file exists
   170  func ExistsFile(path string) (bool, error) {
   171  	_, err := os.Stat(path)
   172  	if err == nil {
   173  		return true, nil
   174  	}
   175  	if os.IsNotExist(err) {
   176  		return false, nil
   177  	}
   178  	return false, err
   179  }
   180  
   181  // ValidConfig does a purely logical validation of the remote config
   182  func ValidConfig(conf *terraform.RemoteState) error {
   183  	// Default the type to Atlas
   184  	if conf.Type == "" {
   185  		conf.Type = "atlas"
   186  	}
   187  	_, err := NewClientByState(conf)
   188  	if err != nil {
   189  		return err
   190  	}
   191  	return nil
   192  }
   193  
   194  // ReadLocalState is used to read and parse the local state file
   195  func ReadLocalState() (*terraform.State, []byte, error) {
   196  	path, err := HiddenStatePath()
   197  	if err != nil {
   198  		return nil, nil, err
   199  	}
   200  
   201  	// Open the existing file
   202  	raw, err := ioutil.ReadFile(path)
   203  	if err != nil {
   204  		if os.IsNotExist(err) {
   205  			return nil, nil, nil
   206  		}
   207  		return nil, nil, fmt.Errorf("Failed to open state file '%s': %s", path, err)
   208  	}
   209  
   210  	// Decode the state
   211  	state, err := terraform.ReadState(bytes.NewReader(raw))
   212  	if err != nil {
   213  		return nil, nil, fmt.Errorf("Failed to read state file '%s': %v", path, err)
   214  	}
   215  	return state, raw, nil
   216  }
   217  
   218  // RefreshState is used to read the remote state given
   219  // the configuration for the remote endpoint, and update
   220  // the local state if necessary.
   221  func RefreshState(conf *terraform.RemoteState) (StateChangeResult, error) {
   222  	if conf == nil {
   223  		return StateChangeNoop, fmt.Errorf("Missing remote server configuration")
   224  	}
   225  
   226  	// Read the state from the server
   227  	client, err := NewClientByState(conf)
   228  	if err != nil {
   229  		return StateChangeNoop,
   230  			fmt.Errorf("Failed to create remote client: %v", err)
   231  	}
   232  	payload, err := client.GetState()
   233  	if err != nil {
   234  		return StateChangeNoop,
   235  			fmt.Errorf("Failed to read remote state: %v", err)
   236  	}
   237  
   238  	// Parse the remote state
   239  	var remoteState *terraform.State
   240  	if payload != nil {
   241  		remoteState, err = terraform.ReadState(bytes.NewReader(payload.State))
   242  		if err != nil {
   243  			return StateChangeNoop,
   244  				fmt.Errorf("Failed to parse remote state: %v", err)
   245  		}
   246  
   247  		// Ensure we understand the remote version!
   248  		if remoteState.Version > terraform.StateVersion {
   249  			return StateChangeNoop, fmt.Errorf(
   250  				`Remote state is version %d, this version of Terraform only understands up to %d`, remoteState.Version, terraform.StateVersion)
   251  		}
   252  	}
   253  
   254  	// Decode the state
   255  	localState, raw, err := ReadLocalState()
   256  	if err != nil {
   257  		return StateChangeNoop, err
   258  	}
   259  
   260  	// We need to handle the matrix of cases in reconciling
   261  	// the local and remote state. Primarily the concern is
   262  	// around the Serial number which should grow monotonically.
   263  	// Additionally, we use the MD5 to detect a conflict for
   264  	// a given Serial.
   265  	switch {
   266  	case remoteState == nil && localState == nil:
   267  		// Initialize a blank state
   268  		out, _ := blankState(conf)
   269  		if err := Persist(bytes.NewReader(out)); err != nil {
   270  			return StateChangeNoop,
   271  				fmt.Errorf("Failed to persist state: %v", err)
   272  		}
   273  		return StateChangeInit, nil
   274  
   275  	case remoteState == nil && localState != nil:
   276  		// User should probably do a push, nothing to do
   277  		return StateChangeLocalNewer, nil
   278  
   279  	case remoteState != nil && localState == nil:
   280  		goto PERSIST
   281  
   282  	case remoteState.Serial < localState.Serial:
   283  		// User should probably do a push, nothing to do
   284  		return StateChangeLocalNewer, nil
   285  
   286  	case remoteState.Serial > localState.Serial:
   287  		goto PERSIST
   288  
   289  	case remoteState.Serial == localState.Serial:
   290  		// Check for a hash collision on the local/remote state
   291  		localMD5 := md5.Sum(raw)
   292  		if bytes.Equal(localMD5[:md5.Size], payload.MD5) {
   293  			// Hash collision, everything is up-to-date
   294  			return StateChangeNoop, nil
   295  		} else {
   296  			// This is very bad. This means we have 2 state files
   297  			// with the same Serial but a different hash. Most probably
   298  			// explaination is two parallel apply operations. This
   299  			// requires a manual reconciliation.
   300  			return StateChangeConflict, nil
   301  		}
   302  	default:
   303  		// We should not reach this point
   304  		panic("Unhandled remote update case")
   305  	}
   306  
   307  PERSIST:
   308  	// Update the local state from the remote state
   309  	if err := Persist(bytes.NewReader(payload.State)); err != nil {
   310  		return StateChangeNoop,
   311  			fmt.Errorf("Failed to persist state: %v", err)
   312  	}
   313  	return StateChangeUpdateLocal, nil
   314  }
   315  
   316  // PushState is used to read the local state and
   317  // update the remote state if necessary. The state push
   318  // can be 'forced' to override any conflict detection
   319  // on the server-side.
   320  func PushState(conf *terraform.RemoteState, force bool) (StateChangeResult, error) {
   321  	// Read the local state
   322  	_, raw, err := ReadLocalState()
   323  	if err != nil {
   324  		return StateChangeNoop, err
   325  	}
   326  
   327  	// Check if there is no local state
   328  	if raw == nil {
   329  		return StateChangeNoop, fmt.Errorf("No local state to push")
   330  	}
   331  
   332  	// Push the state to the server
   333  	client, err := NewClientByState(conf)
   334  	if err != nil {
   335  		return StateChangeNoop,
   336  			fmt.Errorf("Failed to create remote client: %v", err)
   337  	}
   338  	err = client.PutState(raw, force)
   339  
   340  	// Handle the various edge cases
   341  	switch err {
   342  	case nil:
   343  		return StateChangeUpdateRemote, nil
   344  	case ErrServerNewer:
   345  		return StateChangeRemoteNewer, nil
   346  	case ErrConflict:
   347  		return StateChangeConflict, nil
   348  	default:
   349  		return StateChangeNoop, err
   350  	}
   351  }
   352  
   353  // DeleteState is used to delete the remote state given
   354  // the configuration for the remote endpoint.
   355  func DeleteState(conf *terraform.RemoteState) error {
   356  	if conf == nil {
   357  		return fmt.Errorf("Missing remote server configuration")
   358  	}
   359  
   360  	// Setup the client
   361  	client, err := NewClientByState(conf)
   362  	if err != nil {
   363  		return fmt.Errorf("Failed to create remote client: %v", err)
   364  	}
   365  
   366  	// Destroy the state
   367  	err = client.DeleteState()
   368  	if err != nil {
   369  		return fmt.Errorf("Failed to delete remote state: %v", err)
   370  	}
   371  	return nil
   372  }
   373  
   374  // blankState is used to return a serialized form of a blank state
   375  // with only the remote info.
   376  func blankState(conf *terraform.RemoteState) ([]byte, error) {
   377  	blank := terraform.NewState()
   378  	blank.Remote = conf
   379  	buf := bytes.NewBuffer(nil)
   380  	err := terraform.WriteState(blank, buf)
   381  	return buf.Bytes(), err
   382  }
   383  
   384  // PersistState is used to persist out the given terraform state
   385  // in our local state cache location.
   386  func PersistState(s *terraform.State) error {
   387  	buf := bytes.NewBuffer(nil)
   388  	if err := terraform.WriteState(s, buf); err != nil {
   389  		return fmt.Errorf("Failed to encode state: %v", err)
   390  	}
   391  	if err := Persist(buf); err != nil {
   392  		return err
   393  	}
   394  	return nil
   395  }
   396  
   397  // Persist is used to write out the state given by a reader (likely
   398  // being streamed from a remote server) to the local storage.
   399  func Persist(r io.Reader) error {
   400  	cwd, err := os.Getwd()
   401  	if err != nil {
   402  		return fmt.Errorf("Failed to get current directory: %v", err)
   403  	}
   404  	statePath := filepath.Join(cwd, LocalDirectory, HiddenStateFile)
   405  	backupPath := filepath.Join(cwd, LocalDirectory, BackupHiddenStateFile)
   406  
   407  	// Backup the old file if it exists
   408  	if err := CopyFile(statePath, backupPath); err != nil {
   409  		return fmt.Errorf("Failed to backup state file '%s' to '%s': %v", statePath, backupPath, err)
   410  	}
   411  
   412  	// Open the state path
   413  	fh, err := os.Create(statePath)
   414  	if err != nil {
   415  		return fmt.Errorf("Failed to open state file '%s': %v", statePath, err)
   416  	}
   417  
   418  	// Copy the new state
   419  	_, err = io.Copy(fh, r)
   420  	fh.Close()
   421  	if err != nil {
   422  		os.Remove(statePath)
   423  		return fmt.Errorf("Failed to persist state file: %v", err)
   424  	}
   425  	return nil
   426  }
   427  
   428  // CopyFile is used to copy from a source file if it exists to a destination.
   429  // This is used to create a backup of the state file.
   430  func CopyFile(src, dst string) error {
   431  	srcFH, err := os.Open(src)
   432  	if err != nil {
   433  		if os.IsNotExist(err) {
   434  			return nil
   435  		}
   436  		return err
   437  	}
   438  	defer srcFH.Close()
   439  
   440  	dstFH, err := os.Create(dst)
   441  	if err != nil {
   442  		return err
   443  	}
   444  	defer dstFH.Close()
   445  
   446  	_, err = io.Copy(dstFH, srcFH)
   447  	return err
   448  }