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

     1  package command
     2  
     3  import (
     4  	"bytes"
     5  	"flag"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"log"
     9  	"os"
    10  	"strings"
    11  
    12  	"github.com/hashicorp/terraform/remote"
    13  	"github.com/hashicorp/terraform/terraform"
    14  )
    15  
    16  // remoteCommandConfig is used to encapsulate our configuration
    17  type remoteCommandConfig struct {
    18  	disableRemote bool
    19  	pullOnDisable bool
    20  
    21  	statePath  string
    22  	backupPath string
    23  }
    24  
    25  // RemoteCommand is a Command implementation that is used to
    26  // enable and disable remote state management
    27  type RemoteCommand struct {
    28  	Meta
    29  	conf       remoteCommandConfig
    30  	remoteConf terraform.RemoteState
    31  }
    32  
    33  func (c *RemoteCommand) Run(args []string) int {
    34  	args = c.Meta.process(args, false)
    35  	var address, accessToken, name, path string
    36  	cmdFlags := flag.NewFlagSet("remote", flag.ContinueOnError)
    37  	cmdFlags.BoolVar(&c.conf.disableRemote, "disable", false, "")
    38  	cmdFlags.BoolVar(&c.conf.pullOnDisable, "pull", true, "")
    39  	cmdFlags.StringVar(&c.conf.statePath, "state", DefaultStateFilename, "path")
    40  	cmdFlags.StringVar(&c.conf.backupPath, "backup", "", "path")
    41  	cmdFlags.StringVar(&c.remoteConf.Type, "backend", "atlas", "")
    42  	cmdFlags.StringVar(&address, "address", "", "")
    43  	cmdFlags.StringVar(&accessToken, "access-token", "", "")
    44  	cmdFlags.StringVar(&name, "name", "", "")
    45  	cmdFlags.StringVar(&path, "path", "", "")
    46  	cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
    47  	if err := cmdFlags.Parse(args); err != nil {
    48  		return 1
    49  	}
    50  
    51  	// Show help if given no inputs
    52  	if !c.conf.disableRemote && c.remoteConf.Type == "atlas" &&
    53  		name == "" && accessToken == "" {
    54  		cmdFlags.Usage()
    55  		return 1
    56  	}
    57  
    58  	// Populate the various configurations
    59  	c.remoteConf.Config = map[string]string{
    60  		"address":      address,
    61  		"access_token": accessToken,
    62  		"name":         name,
    63  		"path":         path,
    64  	}
    65  
    66  	// Check if have an existing local state file
    67  	haveLocal, err := remote.HaveLocalState()
    68  	if err != nil {
    69  		c.Ui.Error(fmt.Sprintf("Failed to check for local state: %v", err))
    70  		return 1
    71  	}
    72  
    73  	// Check if we have the non-managed state file
    74  	haveNonManaged, err := remote.ExistsFile(c.conf.statePath)
    75  	if err != nil {
    76  		c.Ui.Error(fmt.Sprintf("Failed to check for state file: %v", err))
    77  		return 1
    78  	}
    79  
    80  	// Check if remote state is being disabled
    81  	if c.conf.disableRemote {
    82  		if !haveLocal {
    83  			c.Ui.Error(fmt.Sprintf("Remote state management not enabled! Aborting."))
    84  			return 1
    85  		}
    86  		if haveNonManaged {
    87  			c.Ui.Error(fmt.Sprintf("State file already exists at '%s'. Aborting.",
    88  				c.conf.statePath))
    89  			return 1
    90  		}
    91  		return c.disableRemoteState()
    92  	}
    93  
    94  	// Ensure there is no conflict
    95  	switch {
    96  	case haveLocal && haveNonManaged:
    97  		c.Ui.Error(fmt.Sprintf("Remote state is enabled, but non-managed state file '%s' is also present!",
    98  			c.conf.statePath))
    99  		return 1
   100  
   101  	case !haveLocal && !haveNonManaged:
   102  		// If we don't have either state file, initialize a blank state file
   103  		return c.initBlankState()
   104  
   105  	case haveLocal && !haveNonManaged:
   106  		// Update the remote state target potentially
   107  		return c.updateRemoteConfig()
   108  
   109  	case !haveLocal && haveNonManaged:
   110  		// Enable remote state management
   111  		return c.enableRemoteState()
   112  
   113  	default:
   114  		panic("unhandled case")
   115  	}
   116  	return 0
   117  }
   118  
   119  // disableRemoteState is used to disable remote state management,
   120  // and move the state file into place.
   121  func (c *RemoteCommand) disableRemoteState() int {
   122  	// Get the local state
   123  	local, _, err := remote.ReadLocalState()
   124  	if err != nil {
   125  		c.Ui.Error(fmt.Sprintf("Failed to read local state: %v", err))
   126  		return 1
   127  	}
   128  
   129  	// Ensure we have the latest state before disabling
   130  	if c.conf.pullOnDisable {
   131  		log.Printf("[INFO] Refreshing local state from remote server")
   132  		change, err := remote.RefreshState(local.Remote)
   133  		if err != nil {
   134  			c.Ui.Error(fmt.Sprintf(
   135  				"Failed to refresh from remote state: %v", err))
   136  			return 1
   137  		}
   138  
   139  		// Exit if we were unable to update
   140  		if !change.SuccessfulPull() {
   141  			c.Ui.Error(fmt.Sprintf("%s", change))
   142  			return 1
   143  		} else {
   144  			log.Printf("[INFO] %s", change)
   145  		}
   146  
   147  		// Reload the local state after the refresh
   148  		local, _, err = remote.ReadLocalState()
   149  		if err != nil {
   150  			c.Ui.Error(fmt.Sprintf("Failed to read local state: %v", err))
   151  			return 1
   152  		}
   153  	}
   154  
   155  	// Clear the remote management, and copy into place
   156  	local.Remote = nil
   157  	fh, err := os.Create(c.conf.statePath)
   158  	if err != nil {
   159  		c.Ui.Error(fmt.Sprintf("Failed to create state file '%s': %v",
   160  			c.conf.statePath, err))
   161  		return 1
   162  	}
   163  	defer fh.Close()
   164  	if err := terraform.WriteState(local, fh); err != nil {
   165  		c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %v",
   166  			c.conf.statePath, err))
   167  		return 1
   168  	}
   169  
   170  	// Remove the old state file
   171  	path, err := remote.HiddenStatePath()
   172  	if err != nil {
   173  		c.Ui.Error(fmt.Sprintf("Failed to get local state path: %v", err))
   174  		return 1
   175  	}
   176  	if err := os.Remove(path); err != nil {
   177  		c.Ui.Error(fmt.Sprintf("Failed to remove the local state file: %v", err))
   178  		return 1
   179  	}
   180  	return 0
   181  }
   182  
   183  // validateRemoteConfig is used to verify that the remote configuration
   184  // we have is valid
   185  func (c *RemoteCommand) validateRemoteConfig() error {
   186  	err := remote.ValidConfig(&c.remoteConf)
   187  	if err != nil {
   188  		c.Ui.Error(fmt.Sprintf("%s", err))
   189  	}
   190  	return err
   191  }
   192  
   193  // initBlank state is used to initialize a blank state that is
   194  // remote enabled
   195  func (c *RemoteCommand) initBlankState() int {
   196  	// Validate the remote configuration
   197  	if err := c.validateRemoteConfig(); err != nil {
   198  		return 1
   199  	}
   200  
   201  	// Make the hidden directory
   202  	if err := remote.EnsureDirectory(); err != nil {
   203  		c.Ui.Error(fmt.Sprintf("%s", err))
   204  		return 1
   205  	}
   206  
   207  	// Make a blank state, attach the remote configuration
   208  	blank := terraform.NewState()
   209  	blank.Remote = &c.remoteConf
   210  
   211  	// Persist the state
   212  	if err := remote.PersistState(blank); err != nil {
   213  		c.Ui.Error(fmt.Sprintf("Failed to initialize state file: %v", err))
   214  		return 1
   215  	}
   216  
   217  	// Success!
   218  	c.Ui.Output("Initialized blank state with remote state enabled!")
   219  	return 0
   220  }
   221  
   222  // updateRemoteConfig is used to update the configuration of the
   223  // remote state store
   224  func (c *RemoteCommand) updateRemoteConfig() int {
   225  	// Validate the remote configuration
   226  	if err := c.validateRemoteConfig(); err != nil {
   227  		return 1
   228  	}
   229  
   230  	// Read in the local state
   231  	local, _, err := remote.ReadLocalState()
   232  	if err != nil {
   233  		c.Ui.Error(fmt.Sprintf("Failed to read local state: %v", err))
   234  		return 1
   235  	}
   236  
   237  	// Update the configuration
   238  	local.Remote = &c.remoteConf
   239  	if err := remote.PersistState(local); err != nil {
   240  		c.Ui.Error(fmt.Sprintf("%s", err))
   241  		return 1
   242  	}
   243  
   244  	// Success!
   245  	c.Ui.Output("Remote configuration updated")
   246  	return 0
   247  }
   248  
   249  // enableRemoteState is used to enable remote state management
   250  // and to move a state file into place
   251  func (c *RemoteCommand) enableRemoteState() int {
   252  	// Validate the remote configuration
   253  	if err := c.validateRemoteConfig(); err != nil {
   254  		return 1
   255  	}
   256  
   257  	// Make the hidden directory
   258  	if err := remote.EnsureDirectory(); err != nil {
   259  		c.Ui.Error(fmt.Sprintf("%s", err))
   260  		return 1
   261  	}
   262  
   263  	// Read the provided state file
   264  	raw, err := ioutil.ReadFile(c.conf.statePath)
   265  	if err != nil {
   266  		c.Ui.Error(fmt.Sprintf("Failed to read '%s': %v", c.conf.statePath, err))
   267  		return 1
   268  	}
   269  	state, err := terraform.ReadState(bytes.NewReader(raw))
   270  	if err != nil {
   271  		c.Ui.Error(fmt.Sprintf("Failed to decode '%s': %v", c.conf.statePath, err))
   272  		return 1
   273  	}
   274  
   275  	// Backup the state file before we modify it
   276  	backupPath := c.conf.backupPath
   277  	if backupPath != "-" {
   278  		// Provide default backup path if none provided
   279  		if backupPath == "" {
   280  			backupPath = c.conf.statePath + DefaultBackupExtention
   281  		}
   282  
   283  		log.Printf("[INFO] Writing backup state to: %s", backupPath)
   284  		f, err := os.Create(backupPath)
   285  		if err == nil {
   286  			err = terraform.WriteState(state, f)
   287  			f.Close()
   288  		}
   289  		if err != nil {
   290  			c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err))
   291  			return 1
   292  		}
   293  	}
   294  
   295  	// Update the local configuration, move into place
   296  	state.Remote = &c.remoteConf
   297  	if err := remote.PersistState(state); err != nil {
   298  		c.Ui.Error(fmt.Sprintf("%s", err))
   299  		return 1
   300  	}
   301  
   302  	// Remove the state file
   303  	log.Printf("[INFO] Removing state file: %s", c.conf.statePath)
   304  	if err := os.Remove(c.conf.statePath); err != nil {
   305  		c.Ui.Error(fmt.Sprintf("Failed to remove state file '%s': %v",
   306  			c.conf.statePath, err))
   307  		return 1
   308  	}
   309  
   310  	// Success!
   311  	c.Ui.Output("Remote state management enabled")
   312  	return 0
   313  }
   314  
   315  func (c *RemoteCommand) Help() string {
   316  	helpText := `
   317  Usage: terraform remote [options]
   318  
   319    Configures Terraform to use a remote state server. This allows state
   320    to be pulled down when necessary and then pushed to the server when
   321    updated. In this mode, the state file does not need to be stored durably
   322    since the remote server provides the durability.
   323  
   324  Options:
   325  
   326    -address=url           URL of the remote storage server.
   327                           Required for HTTP backend, optional for Atlas and Consul.
   328  
   329    -access-token=token    Authentication token for state storage server.
   330                           Required for Atlas backend, optional for Consul.
   331  
   332    -backend=Atlas         Specifies the type of remote backend. Must be one
   333                           of Atlas, Consul, or HTTP. Defaults to Atlas.
   334  
   335    -backup=path           Path to backup the existing state file before
   336                           modifying. Defaults to the "-state" path with
   337                           ".backup" extension. Set to "-" to disable backup.
   338  
   339    -disable               Disables remote state management and migrates the state
   340                           to the -state path.
   341  
   342    -name=name             Name of the state file in the state storage server.
   343                           Required for Atlas backend.
   344  
   345    -path=path             Path of the remote state in Consul. Required for the
   346                           Consul backend.
   347  
   348    -pull=true             Controls if the remote state is pulled before disabling.
   349                           This defaults to true to ensure the latest state is cached
   350  						 before disabling.
   351  
   352    -state=path            Path to read state. Defaults to "terraform.tfstate"
   353                           unless remote state is enabled.
   354  
   355  `
   356  	return strings.TrimSpace(helpText)
   357  }
   358  
   359  func (c *RemoteCommand) Synopsis() string {
   360  	return "Configures remote state management"
   361  }