diff --git a/command/apply.go b/command/apply.go index 62daea2175..00ea50504e 100644 --- a/command/apply.go +++ b/command/apply.go @@ -3,6 +3,7 @@ package command import ( "bytes" "fmt" + "log" "os" "sort" "strings" @@ -20,7 +21,7 @@ type ApplyCommand struct { func (c *ApplyCommand) Run(args []string) int { var refresh bool - var statePath, stateOutPath string + var statePath, stateOutPath, backupPath string args = c.Meta.process(args) @@ -28,6 +29,7 @@ func (c *ApplyCommand) Run(args []string) int { cmdFlags.BoolVar(&refresh, "refresh", true, "refresh") cmdFlags.StringVar(&statePath, "state", DefaultStateFilename, "path") cmdFlags.StringVar(&stateOutPath, "state-out", "", "path") + cmdFlags.StringVar(&backupPath, "backup", "", "path") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { return 1 @@ -59,6 +61,12 @@ func (c *ApplyCommand) Run(args []string) int { stateOutPath = statePath } + // If we don't specify a backup path, default to state out with + // the extention + if backupPath == "" { + backupPath = stateOutPath + DefaultBackupExtention + } + // Build the context based on the arguments given ctx, planned, err := c.Context(configPath, statePath) if err != nil { @@ -69,6 +77,20 @@ func (c *ApplyCommand) Run(args []string) int { return 1 } + // Create a backup of the state before updating + if backupPath != "-" { + log.Printf("[INFO] Writing backup state to: %s", backupPath) + f, err := os.Create(backupPath) + if err == nil { + defer f.Close() + err = terraform.WriteState(c.State, f) + } + if err != nil { + c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err)) + return 1 + } + } + // Plan if we haven't already if !planned { if refresh { @@ -201,6 +223,10 @@ Usage: terraform apply [options] [dir] Options: + -backup=path Path to backup the existing state file before + modifying. Defaults to the "-state-out" path with + ".backup" extention. Set to "-" to disable backup. + -no-color If specified, output won't contain any color. -refresh=true Update state prior to checking for differences. This diff --git a/command/command.go b/command/command.go index 9d27321d55..faa9cfcc3b 100644 --- a/command/command.go +++ b/command/command.go @@ -10,6 +10,9 @@ import ( // DefaultStateFilename is the default filename used for the state file. const DefaultStateFilename = "terraform.tfstate" +// DefaultBackupExtention is added to the state file to form the path +const DefaultBackupExtention = ".backup" + func validateContext(ctx *terraform.Context, ui cli.Ui) bool { if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 { ui.Output( diff --git a/command/meta.go b/command/meta.go index e0d3acde77..2dda7a9745 100644 --- a/command/meta.go +++ b/command/meta.go @@ -16,6 +16,7 @@ type Meta struct { Color bool ContextOpts *terraform.ContextOpts Ui cli.Ui + State *terraform.State // This can be set by the command itself to provide extra hooks. extraHooks []terraform.Hook @@ -77,6 +78,9 @@ func (m *Meta) Context(path, statePath string) (*terraform.Context, bool, error) } } + // Store the loaded state + m.State = state + config, err := config.LoadDir(path) if err != nil { return nil, false, fmt.Errorf("Error loading config: %s", err) diff --git a/command/plan.go b/command/plan.go index b866482b64..2a5abf3c3d 100644 --- a/command/plan.go +++ b/command/plan.go @@ -17,7 +17,7 @@ type PlanCommand struct { func (c *PlanCommand) Run(args []string) int { var destroy, refresh bool - var outPath, statePath string + var outPath, statePath, backupPath string args = c.Meta.process(args) @@ -26,6 +26,7 @@ func (c *PlanCommand) Run(args []string) int { cmdFlags.BoolVar(&refresh, "refresh", true, "refresh") cmdFlags.StringVar(&outPath, "out", "", "path") cmdFlags.StringVar(&statePath, "state", DefaultStateFilename, "path") + cmdFlags.StringVar(&backupPath, "backup", "", "path") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { return 1 @@ -58,6 +59,12 @@ func (c *PlanCommand) Run(args []string) int { } } + // If we don't specify a backup path, default to state out with + // the extention + if backupPath == "" { + backupPath = statePath + DefaultBackupExtention + } + ctx, _, err := c.Context(path, statePath) if err != nil { c.Ui.Error(err.Error()) @@ -68,6 +75,20 @@ func (c *PlanCommand) Run(args []string) int { } if refresh { + // Create a backup of the state before updating + if backupPath != "-" { + log.Printf("[INFO] Writing backup state to: %s", backupPath) + f, err := os.Create(backupPath) + if err == nil { + defer f.Close() + err = terraform.WriteState(c.State, f) + } + if err != nil { + c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err)) + return 1 + } + } + c.Ui.Output("Refreshing Terraform state prior to plan...\n") if _, err := ctx.Refresh(); err != nil { c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err)) @@ -130,6 +151,10 @@ Usage: terraform plan [options] [dir] Options: + -backup=path Path to backup the existing state file before + modifying. Defaults to the "-state-out" path with + ".backup" extention. Set to "-" to disable backup. + -destroy If set, a plan will be generated to destroy all resources managed by the given configuration and state. diff --git a/command/refresh.go b/command/refresh.go index 93310efa75..186df8e3ef 100644 --- a/command/refresh.go +++ b/command/refresh.go @@ -16,13 +16,14 @@ type RefreshCommand struct { } func (c *RefreshCommand) Run(args []string) int { - var statePath, stateOutPath string + var statePath, stateOutPath, backupPath string args = c.Meta.process(args) cmdFlags := c.Meta.flagSet("refresh") cmdFlags.StringVar(&statePath, "state", DefaultStateFilename, "path") cmdFlags.StringVar(&stateOutPath, "state-out", "", "path") + cmdFlags.StringVar(&backupPath, "backup", "", "path") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { return 1 @@ -50,6 +51,12 @@ func (c *RefreshCommand) Run(args []string) int { stateOutPath = statePath } + // If we don't specify a backup path, default to state out with + // the extention + if backupPath == "" { + backupPath = stateOutPath + DefaultBackupExtention + } + // Verify that the state path exists. The "ContextArg" function below // will actually do this, but we want to provide a richer error message // if possible. @@ -86,6 +93,20 @@ func (c *RefreshCommand) Run(args []string) int { return 1 } + // Create a backup of the state before updating + if backupPath != "-" { + log.Printf("[INFO] Writing backup state to: %s", backupPath) + f, err := os.Create(backupPath) + if err == nil { + defer f.Close() + err = terraform.WriteState(c.State, f) + } + if err != nil { + c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err)) + return 1 + } + } + state, err := ctx.Refresh() if err != nil { c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err)) @@ -119,6 +140,10 @@ Usage: terraform refresh [options] [dir] Options: + -backup=path Path to backup the existing state file before + modifying. Defaults to the "-state-out" path with + ".backup" extention. Set to "-" to disable backup. + -no-color If specified, output won't contain any color. -state=path Path to read and save state (unless state-out