From 4e44443aa39387e4e8e43854e61bc45177331f1a Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Thu, 9 Oct 2014 16:31:56 -0700 Subject: [PATCH] command/remote: Working on the details --- command/remote.go | 302 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 296 insertions(+), 6 deletions(-) diff --git a/command/remote.go b/command/remote.go index 449fb0e9c..a385aaea4 100644 --- a/command/remote.go +++ b/command/remote.go @@ -1,14 +1,290 @@ package command -import "strings" +import ( + "bytes" + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "strings" + + "github.com/hashicorp/terraform/remote" + "github.com/hashicorp/terraform/terraform" +) + +// remoteCommandConfig is used to encapsulate our configuration +type remoteCommandConfig struct { + disableRemote bool + pullOnDisable bool + + statePath string + backupPath string +} // RemoteCommand is a Command implementation that is used to // enable and disable remote state management type RemoteCommand struct { Meta + conf remoteCommandConfig + remoteConf terraform.RemoteState } func (c *RemoteCommand) Run(args []string) int { + args = c.Meta.process(args, false) + cmdFlags := flag.NewFlagSet("remote", flag.ContinueOnError) + cmdFlags.BoolVar(&c.conf.disableRemote, "disable", false, "") + cmdFlags.BoolVar(&c.conf.pullOnDisable, "pull", true, "") + cmdFlags.StringVar(&c.conf.statePath, "state", "", "path") + cmdFlags.StringVar(&c.conf.backupPath, "backup", "", "path") + cmdFlags.StringVar(&c.remoteConf.AuthToken, "auth", "", "") + cmdFlags.StringVar(&c.remoteConf.Name, "name", "", "") + cmdFlags.StringVar(&c.remoteConf.Server, "server", "", "") + cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } + if err := cmdFlags.Parse(args); err != nil { + return 1 + } + + // Check if have an existing local state file + haveLocal, err := remote.HaveLocalState() + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to check for local state: %v", err)) + return 1 + } + + // Check if we have the non-managed state file + haveNonManaged, err := remote.ExistsFile(c.conf.statePath) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to check for state file: %v", err)) + return 1 + } + + // Check if remote state is being disabled + if c.conf.disableRemote { + if !haveLocal { + c.Ui.Error(fmt.Sprintf("Remote state management not enabled! Aborting.")) + return 1 + } + if haveNonManaged { + c.Ui.Error(fmt.Sprintf("State file already exists at '%s'. Aborting.", + c.conf.statePath)) + return 1 + } + return c.disableRemoteState() + } + + // Ensure there is no conflict + switch { + case haveLocal && haveNonManaged: + c.Ui.Error(fmt.Sprintf("Remote state is enabled, but non-managed state file '%s' is also present!", + c.conf.statePath)) + return 1 + + case !haveLocal && !haveNonManaged: + // If we don't have either state file, initialize a blank state file + return c.initBlankState() + + case haveLocal && !haveNonManaged: + // Update the remote state target potentially + return c.updateRemoteConfig() + + case !haveLocal && haveNonManaged: + // Enable remote state management + return c.enableRemoteState() + + default: + panic("unhandled case") + } + return 0 +} + +// pullState is used to refresh our state before disabling remote +// state management +func (c *RemoteCommand) pullState() error { + // TODO + return nil +} + +// disableRemoteState is used to disable remote state management, +// and move the state file into place. +func (c *RemoteCommand) disableRemoteState() int { + // Ensure we have the latest state before disabling + if c.conf.pullOnDisable { + if err := c.pullState(); err != nil { + c.Ui.Error(fmt.Sprintf("%s", err)) + return 1 + } + } + + // Get the local state + local, _, err := remote.ReadLocalState() + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to read local state: %v", err)) + return 1 + } + + // Clear the remote management, and copy into place + local.Remote = nil + fh, err := os.Create(c.conf.statePath) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to create state file '%s': %v", + c.conf.statePath, err)) + return 1 + } + defer fh.Close() + if err := terraform.WriteState(local, fh); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to encode state file: %v", + c.conf.statePath, err)) + return 1 + } + + // Remove the old state file + path, err := remote.HiddenStatePath() + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to get local state path: %v", err)) + return 1 + } + if err := os.Remove(path); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to remove the local state file: %v", err)) + return 1 + } + return 0 +} + +// validateRemoteConfig is used to verify that the remote configuration +// we have is valid +func (c *RemoteCommand) validateRemoteConfig() error { + err := remote.ValidConfig(&c.remoteConf) + if err != nil { + c.Ui.Error(fmt.Sprintf("%s", err)) + } + return err +} + +// initBlank state is used to initialize a blank state that is +// remote enabled +func (c *RemoteCommand) initBlankState() int { + // Validate the remote configuration + if err := c.validateRemoteConfig(); err != nil { + return 1 + } + + // Make the hidden directory + if err := remote.EnsureDirectory(); err != nil { + c.Ui.Error(fmt.Sprintf("%s", err)) + return 1 + } + + // Make a blank state, attach the remote configuration + blank := terraform.NewState() + blank.Remote = &c.remoteConf + + // Persist the state + buf := bytes.NewBuffer(nil) + if err := terraform.WriteState(blank, buf); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to encode state: %v", err)) + return 1 + } + if err := remote.Persist(buf); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to initialize state file: %v", err)) + return 1 + } + + // Success! + c.Ui.Output("Initialized blank state with remote state enabled!") + return 0 +} + +// updateRemoteConfig is used to update the configuration of the +// remote state store +func (c *RemoteCommand) updateRemoteConfig() int { + // Validate the remote configuration + if err := c.validateRemoteConfig(); err != nil { + return 1 + } + + // Read in the local state + local, _, err := remote.ReadLocalState() + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to read local state: %v", err)) + return 1 + } + + // Update the configuration + local.Remote = &c.remoteConf + if err := remote.PersistState(local); err != nil { + c.Ui.Error(fmt.Sprintf("%s", err)) + return 1 + } + + // Success! + c.Ui.Output("Remote configuration updated") + return 0 +} + +// enableRemoteState is used to enable remote state management +// and to move a state file into place +func (c *RemoteCommand) enableRemoteState() int { + // Validate the remote configuration + if err := c.validateRemoteConfig(); err != nil { + return 1 + } + + // Make the hidden directory + if err := remote.EnsureDirectory(); err != nil { + c.Ui.Error(fmt.Sprintf("%s", err)) + return 1 + } + + // Read the provided state file + raw, err := ioutil.ReadFile(c.conf.statePath) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to read '%s': %v", c.conf.statePath, err)) + return 1 + } + state, err := terraform.ReadState(bytes.NewReader(raw)) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to decode '%s': %v", c.conf.statePath, err)) + return 1 + } + + // Backup the state file before we modify it + backupPath := c.conf.backupPath + if backupPath != "-" { + // Provide default backup path if none provided + if backupPath == "" { + backupPath = c.conf.statePath + DefaultBackupExtention + } + + log.Printf("[INFO] Writing backup state to: %s", backupPath) + f, err := os.Create(backupPath) + if err == nil { + err = terraform.WriteState(state, f) + f.Close() + } + if err != nil { + c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err)) + return 1 + } + } + + // Update the local configuration, move into place + state.Remote = &c.remoteConf + if err := remote.PersistState(state); err != nil { + c.Ui.Error(fmt.Sprintf("%s", err)) + return 1 + } + + // Remove the state file + log.Printf("[INFO] Removing state file: %s", c.conf.statePath) + if err := os.Remove(c.conf.statePath); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to remove state file '%s': %v", + c.conf.statePath, err)) + return 1 + } + + // Success! + c.Ui.Output("Remote state management enabled") return 0 } @@ -23,13 +299,27 @@ Usage: terraform remote [options] Options: - -remote=name Name of the state file in the state storage server. - Optional, default does not use remote storage. - - -remote-auth=token Authentication token for state storage server. + -auth=token Authentication token for state storage server. Optional, defaults to blank. - -remote-server=url URL of the remote storage server. + -backup=path Path to backup the existing state file before + modifying. Defaults to the "-state" path with + ".backup" extension. Set to "-" to disable backup. + + -disable Disables remote state management and migrates the state + to the -state path. + + -pull=true Controls if the remote state is pulled before disabling. + This defaults to true to ensure the latest state is cached + before disabling. + + -name=name Name of the state file in the state storage server. + Optional, default does not use remote storage. + + -server=url URL of the remote storage server. + + -state=path Path to read state. Defaults to "terraform.tfstate" + unless remote state is enabled. ` return strings.TrimSpace(helpText)