From ad7b063262265ef5cf3c3d08c80bd9e96aea0f5f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 18 Jan 2017 20:50:45 -0800 Subject: [PATCH] command: convert to use backends --- command/apply.go | 255 ++++------ command/apply_test.go | 70 +-- command/command.go | 42 ++ command/command_test.go | 136 +++++- command/console.go | 45 +- command/format_plan.go | 231 --------- command/format_plan_test.go | 170 ------- command/format_state.go | 152 ------ command/get.go | 38 +- command/graph.go | 66 ++- command/import.go | 46 +- command/init.go | 273 +++++++---- command/init_test.go | 182 ++++++- command/meta.go | 250 ++-------- command/meta_new.go | 101 ++++ command/output.go | 16 +- command/plan.go | 209 +++----- command/plan_test.go | 126 ++++- command/push.go | 112 +++-- command/refresh.go | 124 ++--- command/refresh_test.go | 4 + command/remote.go | 61 --- command/remote_config.go | 385 --------------- command/remote_config_test.go | 449 ------------------ command/remote_pull.go | 86 ---- command/remote_pull_test.go | 116 ----- command/remote_push.go | 96 ---- command/remote_push_test.go | 69 --- command/show.go | 27 +- command/show_test.go | 19 +- command/state_list.go | 14 +- command/state_meta.go | 21 +- command/state_pull.go | 71 +++ command/state_pull_test.go | 39 ++ command/state_push.go | 144 ++++++ command/state_push_test.go | 154 ++++++ command/state_show.go | 14 +- command/taint.go | 17 +- .../.terraform/terraform.tfstate | 22 + .../backend-change/local-state.tfstate | 6 + command/test-fixtures/backend-change/main.tf | 5 + .../.terraform/terraform.tfstate | 28 ++ .../local-state-old.tfstate | 6 + .../local-state.tfstate | 6 + .../backend-changed-with-legacy/main.tf | 5 + .../.terraform/terraform.tfstate | 21 + .../local-state-old.tfstate | 6 + .../test-fixtures/backend-new-legacy/main.tf | 5 + .../local-state.tfstate | 16 + .../backend-new-migrate-existing/main.tf | 5 + .../terraform.tfstate | 16 + .../test-fixtures/backend-new-migrate/main.tf | 5 + .../backend-new-migrate/terraform.tfstate | 16 + command/test-fixtures/backend-new/main.tf | 5 + .../.terraform/terraform.tfstate | 22 + .../local-state.tfstate | 5 + .../backend-plan-backend-empty-config/main.tf | 5 + .../backend-plan-backend-empty/readme.txt | 1 + .../local-state.tfstate | 5 + .../backend-plan-backend-match/readme.txt | 1 + .../local-state.tfstate | 5 + .../local-state.tfstate | 5 + .../backend-plan-legacy-data/main.tf | 1 + .../backend-plan-legacy-data/state.tfstate | 21 + .../backend-plan-legacy/readme.txt | 1 + .../backend-plan-local-match/main.tf | 1 + .../terraform.tfstate | 6 + .../main.tf | 1 + .../terraform.tfstate | 6 + .../backend-plan-local-newer/main.tf | 1 + .../terraform.tfstate | 6 + .../test-fixtures/backend-plan-local/main.tf | 1 + .../.terraform/terraform.tfstate | 28 ++ .../local-state-old.tfstate | 6 + .../local-state.tfstate | 6 + .../backend-unchanged-with-legacy/main.tf | 5 + .../.terraform/terraform.tfstate | 22 + .../backend-unchanged/local-state.tfstate | 6 + .../test-fixtures/backend-unchanged/main.tf | 5 + .../.terraform/terraform.tfstate | 28 ++ .../local-state-old.tfstate | 6 + .../local-state.tfstate | 6 + .../backend-unset-with-legacy/main.tf | 1 + .../.terraform/terraform.tfstate | 22 + .../backend-unset/local-state.tfstate | 6 + command/test-fixtures/backend-unset/main.tf | 1 + .../init-backend-config-file/input.config | 1 + .../init-backend-config-file/main.tf | 3 + command/test-fixtures/init-backend/main.tf | 5 + command/test-fixtures/init-get/foo/main.tf | 1 + command/test-fixtures/init-get/main.tf | 3 + .../plan-out-backend-legacy/main.tf | 3 + .../test-fixtures/plan-out-backend/main.tf | 9 + .../.terraform/terraform.tfstate | 22 + .../local-state.tfstate | 5 + .../state-push-bad-lineage/main.tf | 5 + .../state-push-bad-lineage/replace.tfstate | 5 + .../.terraform/terraform.tfstate | 22 + command/test-fixtures/state-push-good/main.tf | 5 + .../state-push-good/replace.tfstate | 5 + .../.terraform/terraform.tfstate | 22 + .../local-state.tfstate | 5 + .../state-push-replace-match/main.tf | 5 + .../state-push-replace-match/replace.tfstate | 5 + .../.terraform/terraform.tfstate | 22 + .../local-state.tfstate | 5 + .../state-push-serial-newer/main.tf | 5 + .../state-push-serial-newer/replace.tfstate | 5 + .../.terraform/terraform.tfstate | 22 + .../local-state.tfstate | 5 + .../state-push-serial-older/main.tf | 5 + .../state-push-serial-older/replace.tfstate | 5 + command/ui_input.go | 10 + command/untaint.go | 17 +- 114 files changed, 2333 insertions(+), 2744 deletions(-) delete mode 100644 command/format_plan.go delete mode 100644 command/format_plan_test.go delete mode 100644 command/format_state.go create mode 100644 command/meta_new.go delete mode 100644 command/remote.go delete mode 100644 command/remote_config.go delete mode 100644 command/remote_config_test.go delete mode 100644 command/remote_pull.go delete mode 100644 command/remote_pull_test.go delete mode 100644 command/remote_push.go delete mode 100644 command/remote_push_test.go create mode 100644 command/state_pull.go create mode 100644 command/state_pull_test.go create mode 100644 command/state_push.go create mode 100644 command/state_push_test.go create mode 100644 command/test-fixtures/backend-change/.terraform/terraform.tfstate create mode 100644 command/test-fixtures/backend-change/local-state.tfstate create mode 100644 command/test-fixtures/backend-change/main.tf create mode 100644 command/test-fixtures/backend-changed-with-legacy/.terraform/terraform.tfstate create mode 100644 command/test-fixtures/backend-changed-with-legacy/local-state-old.tfstate create mode 100644 command/test-fixtures/backend-changed-with-legacy/local-state.tfstate create mode 100644 command/test-fixtures/backend-changed-with-legacy/main.tf create mode 100644 command/test-fixtures/backend-new-legacy/.terraform/terraform.tfstate create mode 100644 command/test-fixtures/backend-new-legacy/local-state-old.tfstate create mode 100644 command/test-fixtures/backend-new-legacy/main.tf create mode 100644 command/test-fixtures/backend-new-migrate-existing/local-state.tfstate create mode 100644 command/test-fixtures/backend-new-migrate-existing/main.tf create mode 100644 command/test-fixtures/backend-new-migrate-existing/terraform.tfstate create mode 100644 command/test-fixtures/backend-new-migrate/main.tf create mode 100644 command/test-fixtures/backend-new-migrate/terraform.tfstate create mode 100644 command/test-fixtures/backend-new/main.tf create mode 100644 command/test-fixtures/backend-plan-backend-empty-config/.terraform/terraform.tfstate create mode 100644 command/test-fixtures/backend-plan-backend-empty-config/local-state.tfstate create mode 100644 command/test-fixtures/backend-plan-backend-empty-config/main.tf create mode 100644 command/test-fixtures/backend-plan-backend-empty/readme.txt create mode 100644 command/test-fixtures/backend-plan-backend-match/local-state.tfstate create mode 100644 command/test-fixtures/backend-plan-backend-match/readme.txt create mode 100644 command/test-fixtures/backend-plan-backend-mismatch/local-state.tfstate create mode 100644 command/test-fixtures/backend-plan-legacy-data/local-state.tfstate create mode 100644 command/test-fixtures/backend-plan-legacy-data/main.tf create mode 100644 command/test-fixtures/backend-plan-legacy-data/state.tfstate create mode 100644 command/test-fixtures/backend-plan-legacy/readme.txt create mode 100644 command/test-fixtures/backend-plan-local-match/main.tf create mode 100644 command/test-fixtures/backend-plan-local-match/terraform.tfstate create mode 100644 command/test-fixtures/backend-plan-local-mismatch-lineage/main.tf create mode 100644 command/test-fixtures/backend-plan-local-mismatch-lineage/terraform.tfstate create mode 100644 command/test-fixtures/backend-plan-local-newer/main.tf create mode 100644 command/test-fixtures/backend-plan-local-newer/terraform.tfstate create mode 100644 command/test-fixtures/backend-plan-local/main.tf create mode 100644 command/test-fixtures/backend-unchanged-with-legacy/.terraform/terraform.tfstate create mode 100644 command/test-fixtures/backend-unchanged-with-legacy/local-state-old.tfstate create mode 100644 command/test-fixtures/backend-unchanged-with-legacy/local-state.tfstate create mode 100644 command/test-fixtures/backend-unchanged-with-legacy/main.tf create mode 100644 command/test-fixtures/backend-unchanged/.terraform/terraform.tfstate create mode 100644 command/test-fixtures/backend-unchanged/local-state.tfstate create mode 100644 command/test-fixtures/backend-unchanged/main.tf create mode 100644 command/test-fixtures/backend-unset-with-legacy/.terraform/terraform.tfstate create mode 100644 command/test-fixtures/backend-unset-with-legacy/local-state-old.tfstate create mode 100644 command/test-fixtures/backend-unset-with-legacy/local-state.tfstate create mode 100644 command/test-fixtures/backend-unset-with-legacy/main.tf create mode 100644 command/test-fixtures/backend-unset/.terraform/terraform.tfstate create mode 100644 command/test-fixtures/backend-unset/local-state.tfstate create mode 100644 command/test-fixtures/backend-unset/main.tf create mode 100644 command/test-fixtures/init-backend-config-file/input.config create mode 100644 command/test-fixtures/init-backend-config-file/main.tf create mode 100644 command/test-fixtures/init-backend/main.tf create mode 100644 command/test-fixtures/init-get/foo/main.tf create mode 100644 command/test-fixtures/init-get/main.tf create mode 100644 command/test-fixtures/plan-out-backend-legacy/main.tf create mode 100644 command/test-fixtures/plan-out-backend/main.tf create mode 100644 command/test-fixtures/state-push-bad-lineage/.terraform/terraform.tfstate create mode 100644 command/test-fixtures/state-push-bad-lineage/local-state.tfstate create mode 100644 command/test-fixtures/state-push-bad-lineage/main.tf create mode 100644 command/test-fixtures/state-push-bad-lineage/replace.tfstate create mode 100644 command/test-fixtures/state-push-good/.terraform/terraform.tfstate create mode 100644 command/test-fixtures/state-push-good/main.tf create mode 100644 command/test-fixtures/state-push-good/replace.tfstate create mode 100644 command/test-fixtures/state-push-replace-match/.terraform/terraform.tfstate create mode 100644 command/test-fixtures/state-push-replace-match/local-state.tfstate create mode 100644 command/test-fixtures/state-push-replace-match/main.tf create mode 100644 command/test-fixtures/state-push-replace-match/replace.tfstate create mode 100644 command/test-fixtures/state-push-serial-newer/.terraform/terraform.tfstate create mode 100644 command/test-fixtures/state-push-serial-newer/local-state.tfstate create mode 100644 command/test-fixtures/state-push-serial-newer/main.tf create mode 100644 command/test-fixtures/state-push-serial-newer/replace.tfstate create mode 100644 command/test-fixtures/state-push-serial-older/.terraform/terraform.tfstate create mode 100644 command/test-fixtures/state-push-serial-older/local-state.tfstate create mode 100644 command/test-fixtures/state-push-serial-older/main.tf create mode 100644 command/test-fixtures/state-push-serial-older/replace.tfstate diff --git a/command/apply.go b/command/apply.go index 55f44f0b3..0dc51dfa4 100644 --- a/command/apply.go +++ b/command/apply.go @@ -2,15 +2,16 @@ package command import ( "bytes" + "context" "fmt" "os" "sort" "strings" "github.com/hashicorp/go-getter" - "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/helper/experiment" + "github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/terraform" ) @@ -43,7 +44,7 @@ func (c *ApplyCommand) Run(args []string) int { cmdFlags.BoolVar(&refresh, "refresh", true, "refresh") cmdFlags.IntVar( &c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism") - cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") + cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path") cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } @@ -51,32 +52,25 @@ func (c *ApplyCommand) Run(args []string) int { return 1 } - pwd, err := os.Getwd() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) - return 1 - } - - var configPath string - maybeInit := true + // Get the args. The "maybeInit" flag tracks whether we may need to + // initialize the configuration from a remote path. This is true as long + // as we have an argument. args = cmdFlags.Args() - if len(args) > 1 { - c.Ui.Error("The apply command expects at most one argument.") - cmdFlags.Usage() + maybeInit := len(args) == 1 + configPath, err := ModulePath(args) + if err != nil { + c.Ui.Error(err.Error()) return 1 - } else if len(args) == 1 { - configPath = args[0] - } else { - configPath = pwd - maybeInit = false } - // Prepare the extra hooks to count resources - countHook := new(CountHook) - stateHook := new(StateHook) - c.Meta.extraHooks = []terraform.Hook{countHook, stateHook} - if !c.Destroy && maybeInit { + // We need the pwd for the getter operation below + pwd, err := os.Getwd() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) + return 1 + } + // Do a detect to determine if we need to do an init + apply. if detected, err := getter.Detect(configPath, pwd, getter.Detectors); err != nil { c.Ui.Error(fmt.Sprintf( @@ -96,37 +90,58 @@ func (c *ApplyCommand) Run(args []string) int { } } - terraform.SetDebugInfo(DefaultDataDir) - - // Check for the legacy graph - if experiment.Enabled(experiment.X_legacyGraph) { - c.Ui.Output(c.Colorize().Color( - "[reset][bold][yellow]" + - "Legacy graph enabled! This will use the graph from Terraform 0.7.x\n" + - "to execute this operation. This will be removed in the future so\n" + - "please report any issues causing you to use this to the Terraform\n" + - "project.\n\n")) - } - - // This is going to keep track of shadow errors - var shadowErr error - - // Build the context based on the arguments given - ctx, planned, err := c.Context(contextOpts{ - Destroy: c.Destroy, - Path: configPath, - StatePath: c.Meta.statePath, - Parallelism: c.Meta.parallelism, - }) + // Check if the path is a plan + plan, err := c.Plan(configPath) if err != nil { c.Ui.Error(err.Error()) return 1 } - if c.Destroy && planned { + if c.Destroy && plan != nil { c.Ui.Error(fmt.Sprintf( "Destroy can't be called with a plan file.")) return 1 } + if plan != nil { + // Reset the config path for backend loading + configPath = "" + } + + // Load the module if we don't have one yet (not running from plan) + var mod *module.Tree + if plan == nil { + mod, err = c.Module(configPath) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err)) + return 1 + } + } + + /* + terraform.SetDebugInfo(DefaultDataDir) + + // Check for the legacy graph + if experiment.Enabled(experiment.X_legacyGraph) { + c.Ui.Output(c.Colorize().Color( + "[reset][bold][yellow]" + + "Legacy graph enabled! This will use the graph from Terraform 0.7.x\n" + + "to execute this operation. This will be removed in the future so\n" + + "please report any issues causing you to use this to the Terraform\n" + + "project.\n\n")) + } + */ + + // Load the backend + b, err := c.Backend(&BackendOpts{ + ConfigPath: configPath, + Plan: plan, + }) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + + // If we're not forcing and we're destroying, verify with the + // user at this point. if !destroyForce && c.Destroy { // Default destroy message desc := "Terraform will delete all your managed infrastructure.\n" + @@ -159,80 +174,32 @@ func (c *ApplyCommand) Run(args []string) int { return 1 } } - if !planned { - if err := ctx.Input(c.InputMode()); err != nil { - c.Ui.Error(fmt.Sprintf("Error configuring: %s", err)) - return 1 - } - // Record any shadow errors for later - if err := ctx.ShadowError(); err != nil { - shadowErr = multierror.Append(shadowErr, multierror.Prefix( - err, "input operation:")) - } - } - if !validateContext(ctx, c.Ui) { + // Build the operation + opReq := c.Operation() + opReq.Destroy = c.Destroy + opReq.Module = mod + opReq.Plan = plan + opReq.PlanRefresh = refresh + opReq.Type = backend.OperationTypeApply + + // Perform the operation + ctx, ctxCancel := context.WithCancel(context.Background()) + defer ctxCancel() + op, err := b.Operation(ctx, opReq) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error starting operation: %s", err)) return 1 } - // Plan if we haven't already - if !planned { - if refresh { - if _, err := ctx.Refresh(); err != nil { - c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err)) - return 1 - } - } - - if _, err := ctx.Plan(); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error creating plan: %s", err)) - return 1 - } - - // Record any shadow errors for later - if err := ctx.ShadowError(); err != nil { - shadowErr = multierror.Append(shadowErr, multierror.Prefix( - err, "plan operation:")) - } - } - - // Setup the state hook for continuous state updates - { - state, err := c.State() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error reading state: %s", err)) - return 1 - } - - stateHook.State = state - } - - // Start the apply in a goroutine so that we can be interrupted. - var state *terraform.State - var applyErr error - doneCh := make(chan struct{}) - go func() { - defer close(doneCh) - state, applyErr = ctx.Apply() - - // Record any shadow errors for later - if err := ctx.ShadowError(); err != nil { - shadowErr = multierror.Append(shadowErr, multierror.Prefix( - err, "apply operation:")) - } - }() - - // Wait for the apply to finish or for us to be interrupted so - // we can handle it properly. - err = nil + // Wait for the operation to complete or an interrupt to occur select { case <-c.ShutdownCh: - c.Ui.Output("Interrupt received. Gracefully shutting down...") + // Cancel our context so we can start gracefully exiting + ctxCancel() - // Stop execution - go ctx.Stop() + // Notify the user + c.Ui.Output("Interrupt received. Gracefully shutting down...") // Still get the result, since there is still one select { @@ -241,65 +208,27 @@ func (c *ApplyCommand) Run(args []string) int { "Two interrupts received. Exiting immediately. Note that data\n" + "loss may have occurred.") return 1 - case <-doneCh: + case <-op.Done(): } - case <-doneCh: - } - - // Persist the state - if state != nil { - if err := c.Meta.PersistState(state); err != nil { - c.Ui.Error(fmt.Sprintf("Failed to save state: %s", err)) + case <-op.Done(): + if err := op.Err; err != nil { + c.Ui.Error(err.Error()) return 1 } } - if applyErr != nil { - c.Ui.Error(fmt.Sprintf( - "Error applying plan:\n\n"+ - "%s\n\n"+ - "Terraform does not automatically rollback in the face of errors.\n"+ - "Instead, your Terraform state file has been partially updated with\n"+ - "any resources that successfully completed. Please address the error\n"+ - "above and apply again to incrementally change your infrastructure.", - multierror.Flatten(applyErr))) - return 1 - } - - if c.Destroy { - c.Ui.Output(c.Colorize().Color(fmt.Sprintf( - "[reset][bold][green]\n"+ - "Destroy complete! Resources: %d destroyed.", - countHook.Removed))) - } else { - c.Ui.Output(c.Colorize().Color(fmt.Sprintf( - "[reset][bold][green]\n"+ - "Apply complete! Resources: %d added, %d changed, %d destroyed.", - countHook.Added, - countHook.Changed, - countHook.Removed))) - } - - if countHook.Added > 0 || countHook.Changed > 0 { - c.Ui.Output(c.Colorize().Color(fmt.Sprintf( - "[reset]\n"+ - "The state of your infrastructure has been saved to the path\n"+ - "below. This state is required to modify and destroy your\n"+ - "infrastructure, so keep it safe. To inspect the complete state\n"+ - "use the `terraform show` command.\n\n"+ - "State path: %s", - c.Meta.StateOutPath()))) - } - if !c.Destroy { - if outputs := outputsAsString(state, terraform.RootModulePath, ctx.Module().Config().Outputs, true); outputs != "" { + // Get the right module that we used. If we ran a plan, then use + // that module. + if plan != nil { + mod = plan.Module + } + + if outputs := outputsAsString(op.State, terraform.RootModulePath, mod.Config().Outputs, true); outputs != "" { c.Ui.Output(c.Colorize().Color(outputs)) } } - // If we have an error in the shadow graph, let the user know. - c.outputShadowError(shadowErr, applyErr == nil) - return 0 } diff --git a/command/apply_test.go b/command/apply_test.go index 31f60fb55..77667ae3e 100644 --- a/command/apply_test.go +++ b/command/apply_test.go @@ -519,7 +519,7 @@ func TestApply_plan(t *testing.T) { } args := []string{ - "-state", statePath, + "-state-out", statePath, planPath, } if code := c.Run(args); code != 0 { @@ -564,7 +564,7 @@ func TestApply_plan_backup(t *testing.T) { } args := []string{ - "-state", statePath, + "-state-out", statePath, "-backup", backupPath, planPath, } @@ -601,7 +601,7 @@ func TestApply_plan_noBackup(t *testing.T) { } args := []string{ - "-state", statePath, + "-state-out", statePath, "-backup", "-", planPath, } @@ -670,12 +670,13 @@ func TestApply_plan_remoteState(t *testing.T) { // State file should be not be installed if _, err := os.Stat(filepath.Join(tmp, DefaultStateFilename)); err == nil { - t.Fatalf("State path should not exist") + data, _ := ioutil.ReadFile(DefaultStateFilename) + t.Fatalf("State path should not exist: %s", string(data)) } - // Check for remote state - if _, err := os.Stat(remoteStatePath); err != nil { - t.Fatalf("missing remote state: %s", err) + // Check that there is no remote state config + if _, err := os.Stat(remoteStatePath); err == nil { + t.Fatalf("has remote state config") } } @@ -710,7 +711,7 @@ func TestApply_planWithVarFile(t *testing.T) { } args := []string{ - "-state", statePath, + "-state-out", statePath, planPath, } if code := c.Run(args); code != 0 { @@ -1489,59 +1490,6 @@ func TestApply_disableBackup(t *testing.T) { } } -// -state-out wasn't taking effect when a plan is supplied. GH-7264 -func TestApply_stateOutWithPlan(t *testing.T) { - p := testProvider() - ui := new(cli.MockUi) - - tmpDir := testTempDir(t) - defer os.RemoveAll(tmpDir) - - statePath := filepath.Join(tmpDir, "state.tfstate") - planPath := filepath.Join(tmpDir, "terraform.tfplan") - - args := []string{ - "-state", statePath, - "-out", planPath, - testFixturePath("plan"), - } - - // Run plan first to get a current plan file - pc := &PlanCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, - } - if code := pc.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - // now run apply with the generated plan - stateOutPath := filepath.Join(tmpDir, "state-new.tfstate") - - args = []string{ - "-state", statePath, - "-state-out", stateOutPath, - planPath, - } - - ac := &ApplyCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(p), - Ui: ui, - }, - } - if code := ac.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - // now make sure we wrote out our new state - if _, err := os.Stat(stateOutPath); err != nil { - t.Fatalf("missing new state file: %s", err) - } -} - func testHttpServer(t *testing.T) net.Listener { ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { diff --git a/command/command.go b/command/command.go index a4ca00532..35a85d53b 100644 --- a/command/command.go +++ b/command/command.go @@ -3,6 +3,7 @@ package command import ( "fmt" "log" + "os" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" @@ -27,6 +28,47 @@ const DefaultBackupExtension = ".backup" // operations as it walks the dependency graph. const DefaultParallelism = 10 +// ErrUnsupportedLocalOp is the common error message shown for operations +// that require a backend.Local. +const ErrUnsupportedLocalOp = `The configured backend doesn't support this operation. + +The "backend" in Terraform defines how Terraform operates. The default +backend performs all operations locally on your machine. Your configuration +is configured to use a non-local backend. This backend doesn't support this +operation. + +If you want to use the state from the backend but force all other data +(configuration, variables, etc.) to come locally, you can force local +behavior with the "-local" flag. +` + +// ModulePath returns the path to the root module from the CLI args. +// +// This centralizes the logic for any commands that expect a module path +// on their CLI args. This will verify that only one argument is given +// and that it is a path to configuration. +// +// If your command accepts more than one arg, then change the slice bounds +// to pass validation. +func ModulePath(args []string) (string, error) { + // TODO: test + + if len(args) > 1 { + return "", fmt.Errorf("Too many command line arguments. Configuration path expected.") + } + + if len(args) == 0 { + path, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("Error getting pwd: %s", err) + } + + return path, nil + } + + return args[0], nil +} + func validateContext(ctx *terraform.Context, ui cli.Ui) bool { log.Println("[INFO] Validating the context...") ws, es := ctx.Validate() diff --git a/command/command_test.go b/command/command_test.go index 49e616c45..7be56bcda 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -1,10 +1,16 @@ package command import ( + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/json" "flag" "io" "io/ioutil" "log" + "net/http" + "net/http/httptest" "os" "path/filepath" "strings" @@ -164,7 +170,20 @@ func testState() *terraform.State { }, } state.Init() - return state + + // Write and read the state so that it is properly initialized. We + // do this since we didn't call the normal NewState constructor. + var buf bytes.Buffer + if err := terraform.WriteState(state, &buf); err != nil { + panic(err) + } + + result, err := terraform.ReadState(&buf) + if err != nil { + panic(err) + } + + return result } func testStateFile(t *testing.T, s *terraform.State) string { @@ -220,9 +239,8 @@ func testStateFileRemote(t *testing.T, s *terraform.State) string { return path } -// testStateOutput tests that the state at the given path contains -// the expected state string. -func testStateOutput(t *testing.T, path string, expected string) { +// testStateRead reads the state from a file +func testStateRead(t *testing.T, path string) *terraform.State { f, err := os.Open(path) if err != nil { t.Fatalf("err: %s", err) @@ -234,6 +252,13 @@ func testStateOutput(t *testing.T, path string, expected string) { t.Fatalf("err: %s", err) } + return newState +} + +// testStateOutput tests that the state at the given path contains +// the expected state string. +func testStateOutput(t *testing.T, path string, expected string) { + newState := testStateRead(t, path) actual := strings.TrimSpace(newState.String()) expected = strings.TrimSpace(expected) if actual != expected { @@ -401,3 +426,106 @@ func testStdoutCapture(t *testing.T, dst io.Writer) func() { <-doneCh } } + +// testInteractiveInput configures tests so that the answers given are sent +// in order to interactive prompts. The returned function must be called +// in a defer to clean up. +func testInteractiveInput(t *testing.T, answers []string) func() { + // Disable test mode so input is called + test = false + + // Setup reader/writers + testInputResponse = answers + defaultInputReader = bytes.NewBufferString("") + defaultInputWriter = new(bytes.Buffer) + + // Return the cleanup + return func() { + test = true + testInputResponse = nil + } +} + +// testBackendState is used to make a test HTTP server to test a configured +// backend. This returns the complete state that can be saved. Use +// `testStateFileRemote` to write the returned state. +func testBackendState(t *testing.T, s *terraform.State, c int) (*terraform.State, *httptest.Server) { + var b64md5 string + buf := bytes.NewBuffer(nil) + + cb := func(resp http.ResponseWriter, req *http.Request) { + if req.Method == "PUT" { + resp.WriteHeader(c) + return + } + if s == nil { + resp.WriteHeader(404) + return + } + + resp.Header().Set("Content-MD5", b64md5) + resp.Write(buf.Bytes()) + } + + // If a state was given, make sure we calculate the proper b64md5 + if s != nil { + enc := json.NewEncoder(buf) + if err := enc.Encode(s); err != nil { + t.Fatalf("err: %v", err) + } + md5 := md5.Sum(buf.Bytes()) + b64md5 = base64.StdEncoding.EncodeToString(md5[:16]) + } + + srv := httptest.NewServer(http.HandlerFunc(cb)) + + state := terraform.NewState() + state.Backend = &terraform.BackendState{ + Type: "http", + Config: map[string]interface{}{"address": srv.URL}, + Hash: 2529831861221416334, + } + + return state, srv +} + +// testRemoteState is used to make a test HTTP server to return a given +// state file that can be used for testing legacy remote state. +func testRemoteState(t *testing.T, s *terraform.State, c int) (*terraform.RemoteState, *httptest.Server) { + var b64md5 string + buf := bytes.NewBuffer(nil) + + cb := func(resp http.ResponseWriter, req *http.Request) { + if req.Method == "PUT" { + resp.WriteHeader(c) + return + } + if s == nil { + resp.WriteHeader(404) + return + } + + resp.Header().Set("Content-MD5", b64md5) + resp.Write(buf.Bytes()) + } + + srv := httptest.NewServer(http.HandlerFunc(cb)) + remote := &terraform.RemoteState{ + Type: "http", + Config: map[string]string{"address": srv.URL}, + } + + if s != nil { + // Set the remote data + s.Remote = remote + + enc := json.NewEncoder(buf) + if err := enc.Encode(s); err != nil { + t.Fatalf("err: %v", err) + } + md5 := md5.Sum(buf.Bytes()) + b64md5 = base64.StdEncoding.EncodeToString(md5[:16]) + } + + return remote, srv +} diff --git a/command/console.go b/command/console.go index e68570169..4a6420b77 100644 --- a/command/console.go +++ b/command/console.go @@ -3,9 +3,9 @@ package command import ( "bufio" "fmt" - "os" "strings" + "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/helper/wrappedstreams" "github.com/hashicorp/terraform/repl" @@ -30,30 +30,39 @@ func (c *ConsoleCommand) Run(args []string) int { return 1 } - pwd, err := os.Getwd() + configPath, err := ModulePath(cmdFlags.Args()) if err != nil { - c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) + c.Ui.Error(err.Error()) return 1 } - var configPath string - args = cmdFlags.Args() - if len(args) > 1 { - c.Ui.Error("The console command expects at most one argument.") - cmdFlags.Usage() + // Load the module + mod, err := c.Module(configPath) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err)) return 1 - } else if len(args) == 1 { - configPath = args[0] - } else { - configPath = pwd } - // Build the context based on the arguments given - ctx, _, err := c.Context(contextOpts{ - Path: configPath, - PathEmptyOk: true, - StatePath: c.Meta.statePath, - }) + // Load the backend + b, err := c.Backend(&BackendOpts{ConfigPath: configPath}) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + + // We require a local backend + local, ok := b.(backend.Local) + if !ok { + c.Ui.Error(ErrUnsupportedLocalOp) + return 1 + } + + // Build the operation + opReq := c.Operation() + opReq.Module = mod + + // Get the context + ctx, _, err := local.Context(opReq) if err != nil { c.Ui.Error(err.Error()) return 1 diff --git a/command/format_plan.go b/command/format_plan.go deleted file mode 100644 index e05f57536..000000000 --- a/command/format_plan.go +++ /dev/null @@ -1,231 +0,0 @@ -package command - -import ( - "bytes" - "fmt" - "sort" - "strings" - - "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/colorstring" -) - -// FormatPlanOpts are the options for formatting a plan. -type FormatPlanOpts struct { - // Plan is the plan to format. This is required. - Plan *terraform.Plan - - // Color is the colorizer. This is optional. - Color *colorstring.Colorize - - // ModuleDepth is the depth of the modules to expand. By default this - // is zero which will not expand modules at all. - ModuleDepth int -} - -// FormatPlan takes a plan and returns a -func FormatPlan(opts *FormatPlanOpts) string { - p := opts.Plan - if p.Diff == nil || p.Diff.Empty() { - return "This plan does nothing." - } - - if opts.Color == nil { - opts.Color = &colorstring.Colorize{ - Colors: colorstring.DefaultColors, - Reset: false, - } - } - - buf := new(bytes.Buffer) - for _, m := range p.Diff.Modules { - if len(m.Path)-1 <= opts.ModuleDepth || opts.ModuleDepth == -1 { - formatPlanModuleExpand(buf, m, opts) - } else { - formatPlanModuleSingle(buf, m, opts) - } - } - - return strings.TrimSpace(buf.String()) -} - -// formatPlanModuleExpand will output the given module and all of its -// resources. -func formatPlanModuleExpand( - buf *bytes.Buffer, m *terraform.ModuleDiff, opts *FormatPlanOpts) { - // Ignore empty diffs - if m.Empty() { - return - } - - var moduleName string - if !m.IsRoot() { - moduleName = fmt.Sprintf("module.%s", strings.Join(m.Path[1:], ".")) - } - - // We want to output the resources in sorted order to make things - // easier to scan through, so get all the resource names and sort them. - names := make([]string, 0, len(m.Resources)) - for name, _ := range m.Resources { - names = append(names, name) - } - sort.Strings(names) - - // Go through each sorted name and start building the output - for _, name := range names { - rdiff := m.Resources[name] - if rdiff.Empty() { - continue - } - - dataSource := strings.HasPrefix(name, "data.") - - if moduleName != "" { - name = moduleName + "." + name - } - - // Determine the color for the text (green for adding, yellow - // for change, red for delete), and symbol, and output the - // resource header. - color := "yellow" - symbol := "~" - oldValues := true - switch rdiff.ChangeType() { - case terraform.DiffDestroyCreate: - color = "green" - symbol = "-/+" - case terraform.DiffCreate: - color = "green" - symbol = "+" - oldValues = false - - // If we're "creating" a data resource then we'll present it - // to the user as a "read" operation, so it's clear that this - // operation won't change anything outside of the Terraform state. - // Unfortunately by the time we get here we only have the name - // to work with, so we need to cheat and exploit knowledge of the - // naming scheme for data resources. - if dataSource { - symbol = "<=" - color = "cyan" - } - case terraform.DiffDestroy: - color = "red" - symbol = "-" - } - - var extraAttr []string - if rdiff.DestroyTainted { - extraAttr = append(extraAttr, "tainted") - } - if rdiff.DestroyDeposed { - extraAttr = append(extraAttr, "deposed") - } - var extraStr string - if len(extraAttr) > 0 { - extraStr = fmt.Sprintf(" (%s)", strings.Join(extraAttr, ", ")) - } - - buf.WriteString(opts.Color.Color(fmt.Sprintf( - "[%s]%s %s%s\n", - color, symbol, name, extraStr))) - - // Get all the attributes that are changing, and sort them. Also - // determine the longest key so that we can align them all. - keyLen := 0 - keys := make([]string, 0, len(rdiff.Attributes)) - for key, _ := range rdiff.Attributes { - // Skip the ID since we do that specially - if key == "id" { - continue - } - - keys = append(keys, key) - if len(key) > keyLen { - keyLen = len(key) - } - } - sort.Strings(keys) - - // Go through and output each attribute - for _, attrK := range keys { - attrDiff := rdiff.Attributes[attrK] - - v := attrDiff.New - if v == "" && attrDiff.NewComputed { - v = "" - } - - if attrDiff.Sensitive { - v = "" - } - - updateMsg := "" - if attrDiff.RequiresNew && rdiff.Destroy { - updateMsg = opts.Color.Color(" [red](forces new resource)") - } else if attrDiff.Sensitive && oldValues { - updateMsg = opts.Color.Color(" [yellow](attribute changed)") - } - - if oldValues { - var u string - if attrDiff.Sensitive { - u = "" - } else { - u = attrDiff.Old - } - buf.WriteString(fmt.Sprintf( - " %s:%s %#v => %#v%s\n", - attrK, - strings.Repeat(" ", keyLen-len(attrK)), - u, - v, - updateMsg)) - } else { - buf.WriteString(fmt.Sprintf( - " %s:%s %#v%s\n", - attrK, - strings.Repeat(" ", keyLen-len(attrK)), - v, - updateMsg)) - } - } - - // Write the reset color so we don't overload the user's terminal - buf.WriteString(opts.Color.Color("[reset]\n")) - } -} - -// formatPlanModuleSingle will output the given module and all of its -// resources. -func formatPlanModuleSingle( - buf *bytes.Buffer, m *terraform.ModuleDiff, opts *FormatPlanOpts) { - // Ignore empty diffs - if m.Empty() { - return - } - - moduleName := fmt.Sprintf("module.%s", strings.Join(m.Path[1:], ".")) - - // Determine the color for the text (green for adding, yellow - // for change, red for delete), and symbol, and output the - // resource header. - color := "yellow" - symbol := "~" - switch m.ChangeType() { - case terraform.DiffCreate: - color = "green" - symbol = "+" - case terraform.DiffDestroy: - color = "red" - symbol = "-" - } - - buf.WriteString(opts.Color.Color(fmt.Sprintf( - "[%s]%s %s\n", - color, symbol, moduleName))) - buf.WriteString(fmt.Sprintf( - " %d resource(s)", - len(m.Resources))) - buf.WriteString(opts.Color.Color("[reset]\n")) -} diff --git a/command/format_plan_test.go b/command/format_plan_test.go deleted file mode 100644 index d4119e832..000000000 --- a/command/format_plan_test.go +++ /dev/null @@ -1,170 +0,0 @@ -package command - -import ( - "strings" - "testing" - - "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/colorstring" -) - -// Test that a root level data source gets a special plan output on create -func TestFormatPlan_destroyDeposed(t *testing.T) { - plan := &terraform.Plan{ - Diff: &terraform.Diff{ - Modules: []*terraform.ModuleDiff{ - &terraform.ModuleDiff{ - Path: []string{"root"}, - Resources: map[string]*terraform.InstanceDiff{ - "aws_instance.foo": &terraform.InstanceDiff{ - DestroyDeposed: true, - }, - }, - }, - }, - }, - } - opts := &FormatPlanOpts{ - Plan: plan, - Color: &colorstring.Colorize{ - Colors: colorstring.DefaultColors, - Disable: true, - }, - ModuleDepth: 1, - } - - actual := FormatPlan(opts) - - expected := strings.TrimSpace(` -- aws_instance.foo (deposed) - `) - if actual != expected { - t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, actual) - } -} - -// Test that computed fields with an interpolation string get displayed -func TestFormatPlan_displayInterpolations(t *testing.T) { - plan := &terraform.Plan{ - Diff: &terraform.Diff{ - Modules: []*terraform.ModuleDiff{ - &terraform.ModuleDiff{ - Path: []string{"root"}, - Resources: map[string]*terraform.InstanceDiff{ - "aws_instance.foo": &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "computed_field": &terraform.ResourceAttrDiff{ - New: "${aws_instance.other.id}", - NewComputed: true, - }, - }, - }, - }, - }, - }, - }, - } - opts := &FormatPlanOpts{ - Plan: plan, - Color: &colorstring.Colorize{ - Colors: colorstring.DefaultColors, - Disable: true, - }, - ModuleDepth: 1, - } - - out := FormatPlan(opts) - lines := strings.Split(out, "\n") - if len(lines) != 2 { - t.Fatal("expected 2 lines of output, got:\n", out) - } - - actual := strings.TrimSpace(lines[1]) - expected := `computed_field: "" => "${aws_instance.other.id}"` - - if actual != expected { - t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, actual) - } -} - -// Test that a root level data source gets a special plan output on create -func TestFormatPlan_rootDataSource(t *testing.T) { - plan := &terraform.Plan{ - Diff: &terraform.Diff{ - Modules: []*terraform.ModuleDiff{ - &terraform.ModuleDiff{ - Path: []string{"root"}, - Resources: map[string]*terraform.InstanceDiff{ - "data.type.name": &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "A": &terraform.ResourceAttrDiff{ - New: "B", - RequiresNew: true, - }, - }, - }, - }, - }, - }, - }, - } - opts := &FormatPlanOpts{ - Plan: plan, - Color: &colorstring.Colorize{ - Colors: colorstring.DefaultColors, - Disable: true, - }, - ModuleDepth: 1, - } - - actual := FormatPlan(opts) - - expected := strings.TrimSpace(` - <= data.type.name - A: "B" - `) - if actual != expected { - t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, actual) - } -} - -// Test that data sources nested in modules get the same plan output -func TestFormatPlan_nestedDataSource(t *testing.T) { - plan := &terraform.Plan{ - Diff: &terraform.Diff{ - Modules: []*terraform.ModuleDiff{ - &terraform.ModuleDiff{ - Path: []string{"root", "nested"}, - Resources: map[string]*terraform.InstanceDiff{ - "data.type.name": &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "A": &terraform.ResourceAttrDiff{ - New: "B", - RequiresNew: true, - }, - }, - }, - }, - }, - }, - }, - } - opts := &FormatPlanOpts{ - Plan: plan, - Color: &colorstring.Colorize{ - Colors: colorstring.DefaultColors, - Disable: true, - }, - ModuleDepth: 2, - } - - actual := FormatPlan(opts) - - expected := strings.TrimSpace(` - <= module.nested.data.type.name - A: "B" - `) - if actual != expected { - t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, actual) - } -} diff --git a/command/format_state.go b/command/format_state.go deleted file mode 100644 index d54a7648a..000000000 --- a/command/format_state.go +++ /dev/null @@ -1,152 +0,0 @@ -package command - -import ( - "bytes" - "fmt" - "sort" - "strings" - - "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/colorstring" -) - -// FormatStateOpts are the options for formatting a state. -type FormatStateOpts struct { - // State is the state to format. This is required. - State *terraform.State - - // Color is the colorizer. This is optional. - Color *colorstring.Colorize - - // ModuleDepth is the depth of the modules to expand. By default this - // is zero which will not expand modules at all. - ModuleDepth int -} - -// FormatState takes a state and returns a string -func FormatState(opts *FormatStateOpts) string { - if opts.Color == nil { - panic("colorize not given") - } - - s := opts.State - if len(s.Modules) == 0 { - return "The state file is empty. No resources are represented." - } - - var buf bytes.Buffer - buf.WriteString("[reset]") - - // Format all the modules - for _, m := range s.Modules { - if len(m.Path)-1 <= opts.ModuleDepth || opts.ModuleDepth == -1 { - formatStateModuleExpand(&buf, m, opts) - } else { - formatStateModuleSingle(&buf, m, opts) - } - } - - // Write the outputs for the root module - m := s.RootModule() - if len(m.Outputs) > 0 { - buf.WriteString("\nOutputs:\n\n") - - // Sort the outputs - ks := make([]string, 0, len(m.Outputs)) - for k, _ := range m.Outputs { - ks = append(ks, k) - } - sort.Strings(ks) - - // Output each output k/v pair - for _, k := range ks { - v := m.Outputs[k] - switch output := v.Value.(type) { - case string: - buf.WriteString(fmt.Sprintf("%s = %s", k, output)) - buf.WriteString("\n") - case []interface{}: - buf.WriteString(formatListOutput("", k, output)) - buf.WriteString("\n") - case map[string]interface{}: - buf.WriteString(formatMapOutput("", k, output)) - buf.WriteString("\n") - } - } - } - - return opts.Color.Color(strings.TrimSpace(buf.String())) -} - -func formatStateModuleExpand( - buf *bytes.Buffer, m *terraform.ModuleState, opts *FormatStateOpts) { - var moduleName string - if !m.IsRoot() { - moduleName = fmt.Sprintf("module.%s", strings.Join(m.Path[1:], ".")) - } - - // First get the names of all the resources so we can show them - // in alphabetical order. - names := make([]string, 0, len(m.Resources)) - for name, _ := range m.Resources { - names = append(names, name) - } - sort.Strings(names) - - // Go through each resource and begin building up the output. - for _, k := range names { - name := k - if moduleName != "" { - name = moduleName + "." + name - } - - rs := m.Resources[k] - is := rs.Primary - var id string - if is != nil { - id = is.ID - } - if id == "" { - id = "" - } - - taintStr := "" - if rs.Primary != nil && rs.Primary.Tainted { - taintStr = " (tainted)" - } - - buf.WriteString(fmt.Sprintf("%s:%s\n", name, taintStr)) - buf.WriteString(fmt.Sprintf(" id = %s\n", id)) - - if is != nil { - // Sort the attributes - attrKeys := make([]string, 0, len(is.Attributes)) - for ak, _ := range is.Attributes { - // Skip the id attribute since we just show the id directly - if ak == "id" { - continue - } - - attrKeys = append(attrKeys, ak) - } - sort.Strings(attrKeys) - - // Output each attribute - for _, ak := range attrKeys { - av := is.Attributes[ak] - buf.WriteString(fmt.Sprintf(" %s = %s\n", ak, av)) - } - } - } - - buf.WriteString("[reset]\n") -} - -func formatStateModuleSingle( - buf *bytes.Buffer, m *terraform.ModuleState, opts *FormatStateOpts) { - // Header with the module name - buf.WriteString(fmt.Sprintf("module.%s\n", strings.Join(m.Path[1:], "."))) - - // Now just write how many resources are in here. - buf.WriteString(fmt.Sprintf(" %d resource(s)\n", len(m.Resources))) -} diff --git a/command/get.go b/command/get.go index b03852a4b..982484f9b 100644 --- a/command/get.go +++ b/command/get.go @@ -3,7 +3,6 @@ package command import ( "flag" "fmt" - "os" "strings" "github.com/hashicorp/terraform/config/module" @@ -28,19 +27,10 @@ func (c *GetCommand) Run(args []string) int { } var path string - args = cmdFlags.Args() - if len(args) > 1 { - c.Ui.Error("The get command expects one argument.\n") - cmdFlags.Usage() + path, err := ModulePath(cmdFlags.Args()) + if err != nil { + c.Ui.Error(err.Error()) return 1 - } else if len(args) == 1 { - path = args[0] - } else { - var err error - path, err = os.Getwd() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) - } } mode := module.GetModeGet @@ -48,12 +38,8 @@ func (c *GetCommand) Run(args []string) int { mode = module.GetModeUpdate } - _, _, err := c.Context(contextOpts{ - Path: path, - GetMode: mode, - }) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error loading Terraform: %s", err)) + if err := getModules(&c.Meta, path, mode); err != nil { + c.Ui.Error(err.Error()) return 1 } @@ -86,3 +72,17 @@ Options: func (c *GetCommand) Synopsis() string { return "Download and install modules for the configuration" } + +func getModules(m *Meta, path string, mode module.GetMode) error { + mod, err := module.NewTreeModule("", path) + if err != nil { + return fmt.Errorf("Error loading configuration: %s", err) + } + + err = mod.Load(m.moduleStorage(m.DataDir()), mode) + if err != nil { + return fmt.Errorf("Error loading modules: %s", err) + } + + return nil +} diff --git a/command/graph.go b/command/graph.go index b127c54b9..aa5f14d1a 100644 --- a/command/graph.go +++ b/command/graph.go @@ -3,9 +3,10 @@ package command import ( "flag" "fmt" - "os" "strings" + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/dag" "github.com/hashicorp/terraform/terraform" ) @@ -34,34 +35,65 @@ func (c *GraphCommand) Run(args []string) int { return 1 } - var path string - args = cmdFlags.Args() - if len(args) > 1 { - c.Ui.Error("The graph command expects one argument.\n") - cmdFlags.Usage() + configPath, err := ModulePath(cmdFlags.Args()) + if err != nil { + c.Ui.Error(err.Error()) return 1 - } else if len(args) == 1 { - path = args[0] - } else { - var err error - path, err = os.Getwd() + } + + // Check if the path is a plan + plan, err := c.Plan(configPath) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + if plan != nil { + // Reset for backend loading + configPath = "" + } + + // Load the module + var mod *module.Tree + if plan == nil { + mod, err = c.Module(configPath) if err != nil { - c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) + c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err)) + return 1 } } - ctx, planFile, err := c.Context(contextOpts{ - Path: path, - StatePath: "", + // Load the backend + b, err := c.Backend(&BackendOpts{ + ConfigPath: configPath, + Plan: plan, }) if err != nil { - c.Ui.Error(fmt.Sprintf("Error loading Terraform: %s", err)) + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + + // We require a local backend + local, ok := b.(backend.Local) + if !ok { + c.Ui.Error(ErrUnsupportedLocalOp) + return 1 + } + + // Build the operation + opReq := c.Operation() + opReq.Module = mod + opReq.Plan = plan + + // Get the context + ctx, _, err := local.Context(opReq) + if err != nil { + c.Ui.Error(err.Error()) return 1 } // Determine the graph type graphType := terraform.GraphTypePlan - if planFile { + if plan != nil { graphType = terraform.GraphTypeApply } diff --git a/command/import.go b/command/import.go index bcad63a6d..df8e420bd 100644 --- a/command/import.go +++ b/command/import.go @@ -6,6 +6,8 @@ import ( "os" "strings" + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/terraform" ) @@ -45,13 +47,37 @@ func (c *ImportCommand) Run(args []string) int { return 1 } - // Build the context based on the arguments given - ctx, _, err := c.Context(contextOpts{ - Path: configPath, - PathEmptyOk: true, - StatePath: c.Meta.statePath, - Parallelism: c.Meta.parallelism, - }) + // Load the module + var mod *module.Tree + if configPath != "" { + var err error + mod, err = c.Module(configPath) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err)) + return 1 + } + } + + // Load the backend + b, err := c.Backend(&BackendOpts{ConfigPath: configPath}) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + + // We require a local backend + local, ok := b.(backend.Local) + if !ok { + c.Ui.Error(ErrUnsupportedLocalOp) + return 1 + } + + // Build the operation + opReq := c.Operation() + opReq.Module = mod + + // Get the context + ctx, state, err := local.Context(opReq) if err != nil { c.Ui.Error(err.Error()) return 1 @@ -76,7 +102,11 @@ func (c *ImportCommand) Run(args []string) int { // Persist the final state log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath()) - if err := c.Meta.PersistState(newState); err != nil { + if err := state.WriteState(newState); err != nil { + c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) + return 1 + } + if err := state.PersistState(); err != nil { c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) return 1 } diff --git a/command/init.go b/command/init.go index bdee258d7..3ffd2898c 100644 --- a/command/init.go +++ b/command/init.go @@ -1,7 +1,6 @@ package command import ( - "flag" "fmt" "os" "path/filepath" @@ -10,7 +9,6 @@ import ( "github.com/hashicorp/go-getter" "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config/module" - "github.com/hashicorp/terraform/terraform" ) // InitCommand is a Command implementation that takes a Terraform @@ -20,39 +18,48 @@ type InitCommand struct { } func (c *InitCommand) Run(args []string) int { - var remoteBackend string + var flagBackend, flagGet bool + var flagConfigFile string args = c.Meta.process(args, false) - remoteConfig := make(map[string]string) - cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError) - cmdFlags.StringVar(&remoteBackend, "backend", "", "") - cmdFlags.Var((*FlagStringKV)(&remoteConfig), "backend-config", "config") + cmdFlags := c.flagSet("init") + cmdFlags.BoolVar(&flagBackend, "backend", true, "") + cmdFlags.StringVar(&flagConfigFile, "backend-config", "", "") + cmdFlags.BoolVar(&flagGet, "get", true, "") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { return 1 } - remoteBackend = strings.ToLower(remoteBackend) - - var path string + // Validate the arg count args = cmdFlags.Args() if len(args) > 2 { c.Ui.Error("The init command expects at most two arguments.\n") cmdFlags.Usage() return 1 - } else if len(args) < 1 { - c.Ui.Error("The init command expects at least one arguments.\n") - cmdFlags.Usage() + } + + // Get our pwd. We don't always need it but always getting it is easier + // than the logic to determine if it is or isn't needed. + pwd, err := os.Getwd() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) return 1 } - if len(args) == 2 { + // Get the path and source module to copy + var path string + var source string + switch len(args) { + case 0: + path = pwd + case 1: + path = pwd + source = args[0] + case 2: + source = args[0] path = args[1] - } else { - var err error - path, err = os.Getwd() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) - } + default: + panic("assertion failed on arg count") } // Set the state out path to be the path requested for the module @@ -60,110 +67,186 @@ func (c *InitCommand) Run(args []string) int { // proper directory. c.Meta.dataDir = filepath.Join(path, DefaultDataDir) - source := args[0] + // This will track whether we outputted anything so that we know whether + // to output a newline before the success message + var header bool - // Get our pwd since we need it - pwd, err := os.Getwd() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error reading working directory: %s", err)) - return 1 + // If we have a source, copy it + if source != "" { + c.Ui.Output(c.Colorize().Color(fmt.Sprintf( + "[reset][bold]"+ + "Initializing configuration from: %q...", source))) + if err := c.copySource(path, source, pwd); err != nil { + c.Ui.Error(fmt.Sprintf( + "Error copying source: %s", err)) + return 1 + } + + header = true } - // Verify the directory is empty + // If our directory is empty, then we're done. We can't get or setup + // the backend with an empty directory. if empty, err := config.IsEmptyDir(path); err != nil { c.Ui.Error(fmt.Sprintf( - "Error checking on destination path: %s", err)) + "Error checking configuration: %s", err)) return 1 + } else if empty { + c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitEmpty))) + return 0 + } + + // If we're performing a get or loading the backend, then we perform + // some extra tasks. + if flagGet || flagBackend { + // Load the configuration in this directory so that we can know + // if we have anything to get or any backend to configure. We do + // this to improve the UX. Practically, we could call the functions + // below without checking this to the same effect. + conf, err := config.LoadDir(path) + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Error loading configuration: %s", err)) + return 1 + } + + // If we requested downloading modules and have modules in the config + if flagGet && len(conf.Modules) > 0 { + header = true + + c.Ui.Output(c.Colorize().Color(fmt.Sprintf( + "[reset][bold]" + + "Downloading modules (if any)..."))) + if err := getModules(&c.Meta, path, module.GetModeGet); err != nil { + c.Ui.Error(fmt.Sprintf( + "Error downloading modules: %s", err)) + return 1 + } + } + + // If we're requesting backend configuration and configure it + hasBackend := conf.Terraform != nil && conf.Terraform.Backend != nil + if flagBackend && hasBackend { + header = true + + c.Ui.Output(c.Colorize().Color(fmt.Sprintf( + "[reset][bold]" + + "Initializing the backend..."))) + + opts := &BackendOpts{ + ConfigPath: path, + ConfigFile: flagConfigFile, + Init: true, + } + if _, err := c.Backend(opts); err != nil { + c.Ui.Error(err.Error()) + return 1 + } + } + } + + // If we outputted information, then we need to output a newline + // so that our success message is nicely spaced out from prior text. + if header { + c.Ui.Output("") + } + + c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitSuccess))) + + return 0 +} + +func (c *InitCommand) copySource(dst, src, pwd string) error { + // Verify the directory is empty + if empty, err := config.IsEmptyDir(dst); err != nil { + return fmt.Errorf("Error checking on destination path: %s", err) } else if !empty { - c.Ui.Error( - "The destination path has Terraform configuration files. The\n" + - "init command can only be used on a directory without existing Terraform\n" + - "files.") - return 1 + return fmt.Errorf(strings.TrimSpace(errInitCopyNotEmpty)) } // Detect - source, err = getter.Detect(source, pwd, getter.Detectors) + source, err := getter.Detect(src, pwd, getter.Detectors) if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error with module source: %s", err)) - return 1 + return fmt.Errorf("Error with module source: %s", err) } // Get it! - if err := module.GetCopy(path, source); err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - // Handle remote state if configured - if remoteBackend != "" { - var remoteConf terraform.RemoteState - remoteConf.Type = remoteBackend - remoteConf.Config = remoteConfig - - state, err := c.State() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error checking for state: %s", err)) - return 1 - } - if state != nil { - s := state.State() - if !s.Empty() { - c.Ui.Error(fmt.Sprintf( - "State file already exists and is not empty! Please remove this\n" + - "state file before initializing. Note that removing the state file\n" + - "may result in a loss of information since Terraform uses this\n" + - "to track your infrastructure.")) - return 1 - } - if s.IsRemote() { - c.Ui.Error(fmt.Sprintf( - "State file already exists with remote state enabled! Please remove this\n" + - "state file before initializing. Note that removing the state file\n" + - "may result in a loss of information since Terraform uses this\n" + - "to track your infrastructure.")) - return 1 - } - } - - // Initialize a blank state file with remote enabled - remoteCmd := &RemoteConfigCommand{ - Meta: c.Meta, - remoteConf: &remoteConf, - } - return remoteCmd.initBlankState() - } - return 0 + return module.GetCopy(dst, source) } func (c *InitCommand) Help() string { helpText := ` -Usage: terraform init [options] SOURCE [PATH] +Usage: terraform init [options] [SOURCE] [PATH] - Downloads the module given by SOURCE into the PATH. The PATH defaults - to the working directory. PATH must be empty of any Terraform files. - Any conflicting non-Terraform files will be overwritten. + Initialize a new or existing Terraform environment by creating + initial files, loading any remote state, downloading modules, etc. - The module downloaded is a copy. If you're downloading a module from - Git, it will not preserve the Git history, it will only copy the - latest files. + This is the first command that should be run for any new or existing + Terraform configuration per machine. This sets up all the local data + necessary to run Terraform that is typically not comitted to version + control. + + This command is always safe to run multiple times. Though subsequent runs + may give errors, this command will never blow away your environment or state. + Even so, if you have important information, please back it up prior to + running this command just in case. + + If no arguments are given, the configuration in this working directory + is initialized. + + If one or two arguments are given, the first is a SOURCE of a module to + download to the second argument PATH. After downloading the module to PATH, + the configuration will be initialized as if this command were called pointing + only to that PATH. PATH must be empty of any Terraform files. Any + conflicting non-Terraform files will be overwritten. The module download + is a copy. If you're downloading a module from Git, it will not preserve + Git history. Options: - -backend=atlas Specifies the type of remote backend. If not - specified, local storage will be used. + -backend=true Configure the backend for this environment. - -backend-config="k=v" Specifies configuration for the remote storage - backend. This can be specified multiple times. + -backend-config=path A path to load additional configuration for the backend. + This is merged with what is in the configuration file. - -no-color If specified, output won't contain any color. + -get=true Download any modules for this configuration. + + -input=true Ask for input if necessary. If false, will error if + input was required. + + -no-color If specified, output won't contain any color. ` return strings.TrimSpace(helpText) } func (c *InitCommand) Synopsis() string { - return "Initializes Terraform configuration from a module" + return "Initialize a new or existing Terraform configuration" } + +const errInitCopyNotEmpty = ` +The destination path contains Terraform configuration files. The init command +with a SOURCE parameter can only be used on a directory without existing +Terraform files. + +Please resolve this issue and try again. +` + +const outputInitEmpty = ` +[reset][bold]Terraform initialized in an empty directory![reset] + +The directory has no Terraform configuration files. You may begin working +with Terraform immediately by creating Terraform configuration files. +` + +const outputInitSuccess = ` +[reset][bold][green]Terraform has been successfully initialized![reset][green] + +You may now begin working with Terraform. Try running "terraform plan" to see +any changes that are required for your infrastructure. All Terraform commands +should now work. + +If you ever set or change modules or backend configuration for Terraform, +rerun this command to reinitialize your environment. If you forget, other +commands will detect it and remind you to do so if necessary. +` diff --git a/command/init_test.go b/command/init_test.go index c13249166..4c8e4908e 100644 --- a/command/init_test.go +++ b/command/init_test.go @@ -3,9 +3,10 @@ package command import ( "os" "path/filepath" + "strings" "testing" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/helper/copy" "github.com/mitchellh/cli" ) @@ -69,6 +70,27 @@ func TestInit_cwd(t *testing.T) { } } +func TestInit_empty(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + os.MkdirAll(td, 0755) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + ui := new(cli.MockUi) + c := &InitCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } +} + func TestInit_multipleArgs(t *testing.T) { ui := new(cli.MockUi) c := &InitCommand{ @@ -87,21 +109,6 @@ func TestInit_multipleArgs(t *testing.T) { } } -func TestInit_noArgs(t *testing.T) { - ui := new(cli.MockUi) - c := &InitCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, - }, - } - - args := []string{} - if code := c.Run(args); code != 1 { - t.Fatalf("bad: \n%s", ui.OutputWriter.String()) - } -} - // https://github.com/hashicorp/terraform/issues/518 func TestInit_dstInSrc(t *testing.T) { dir := tempDir(t) @@ -144,6 +151,148 @@ func TestInit_dstInSrc(t *testing.T) { } } +func TestInit_get(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("init-get"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + ui := new(cli.MockUi) + c := &InitCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + // Check output + output := ui.OutputWriter.String() + if !strings.Contains(output, "Get: file://") { + t.Fatalf("doesn't look like get: %s", output) + } +} + +func TestInit_copyGet(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + os.MkdirAll(td, 0755) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + ui := new(cli.MockUi) + c := &InitCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{ + testFixturePath("init-get"), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + // Check copy + if _, err := os.Stat("main.tf"); err != nil { + t.Fatalf("err: %s", err) + } + + output := ui.OutputWriter.String() + if !strings.Contains(output, "Get: file://") { + t.Fatalf("doesn't look like get: %s", output) + } +} + +func TestInit_backend(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("init-backend"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + ui := new(cli.MockUi) + c := &InitCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + if _, err := os.Stat(filepath.Join(DefaultDataDir, DefaultStateFilename)); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestInit_backendConfigFile(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("init-backend-config-file"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + ui := new(cli.MockUi) + c := &InitCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{"-backend-config", "input.config"} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + // Read our saved backend config and verify we have our settings + state := testStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) + if v := state.Backend.Config["path"]; v != "hello" { + t.Fatalf("bad: %#v", v) + } +} + +func TestInit_copyBackendDst(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + os.MkdirAll(td, 0755) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + ui := new(cli.MockUi) + c := &InitCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{ + testFixturePath("init-backend"), + "dst", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + if _, err := os.Stat(filepath.Join( + "dst", DefaultDataDir, DefaultStateFilename)); err != nil { + t.Fatalf("err: %s", err) + } +} + +/* func TestInit_remoteState(t *testing.T) { tmp, cwd := testCwd(t) defer testFixCwd(t, tmp, cwd) @@ -287,3 +436,4 @@ func TestInit_remoteStateWithRemote(t *testing.T) { t.Fatalf("should have failed: \n%s", ui.OutputWriter.String()) } } +*/ diff --git a/command/meta.go b/command/meta.go index 5566c2669..64361eeab 100644 --- a/command/meta.go +++ b/command/meta.go @@ -10,16 +10,13 @@ import ( "os" "path/filepath" "strconv" + "strings" "time" - "github.com/hashicorp/errwrap" "github.com/hashicorp/go-getter" - "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/helper/experiment" "github.com/hashicorp/terraform/helper/variables" "github.com/hashicorp/terraform/helper/wrappedstreams" - "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" "github.com/mitchellh/colorstring" @@ -27,21 +24,31 @@ import ( // Meta are the meta-options that are available on all or most commands. type Meta struct { - Color bool - ContextOpts *terraform.ContextOpts - Ui cli.Ui + // The exported fields below should be set by anyone using a + // command with a Meta field. These are expected to be set externally + // (not from within the command itself). - // State read when calling `Context`. This is available after calling - // `Context`. - state state.State - stateResult *StateResult + Color bool // True if output should be colored + ContextOpts *terraform.ContextOpts // Opts copied to initialize + Ui cli.Ui // Ui for output - // This can be set by the command itself to provide extra hooks. - extraHooks []terraform.Hook + // ExtraHooks are extra hooks to add to the context. + ExtraHooks []terraform.Hook - // This can be set by tests to change some directories + //---------------------------------------------------------- + // Protected: commands can set these + //---------------------------------------------------------- + + // Modify the data directory location. Defaults to DefaultDataDir dataDir string + //---------------------------------------------------------- + // Private: do not set these + //---------------------------------------------------------- + + // backendState is the currently active backend state + backendState *terraform.BackendState + // Variables for the context (private) autoKey string autoVariables map[string]interface{} @@ -51,6 +58,7 @@ type Meta struct { // Targets for this context (private) targets []string + // Internal fields color bool oldUi cli.Ui @@ -111,103 +119,6 @@ func (m *Meta) Colorize() *colorstring.Colorize { } } -// Context returns a Terraform Context taking into account the context -// options used to initialize this meta configuration. -func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) { - opts := m.contextOpts() - - // First try to just read the plan directly from the path given. - f, err := os.Open(copts.Path) - if err == nil { - plan, err := terraform.ReadPlan(f) - f.Close() - if err == nil { - // Setup our state, force it to use our plan's state - stateOpts := m.StateOpts() - if plan != nil { - stateOpts.ForceState = plan.State - } - - // Get the state - result, err := State(stateOpts) - if err != nil { - return nil, false, fmt.Errorf("Error loading plan: %s", err) - } - - // Set our state - m.state = result.State - - // this is used for printing the saved location later - if m.stateOutPath == "" { - m.stateOutPath = result.StatePath - } - - if len(m.variables) > 0 { - return nil, false, fmt.Errorf( - "You can't set variables with the '-var' or '-var-file' flag\n" + - "when you're applying a plan file. The variables used when\n" + - "the plan was created will be used. If you wish to use different\n" + - "variable values, create a new plan file.") - } - - ctx, err := plan.Context(opts) - return ctx, true, err - } - } - - // Load the statePath if not given - if copts.StatePath != "" { - m.statePath = copts.StatePath - } - - // Tell the context if we're in a destroy plan / apply - opts.Destroy = copts.Destroy - - // Store the loaded state - state, err := m.State() - if err != nil { - return nil, false, err - } - - // Load the root module - var mod *module.Tree - if copts.Path != "" { - mod, err = module.NewTreeModule("", copts.Path) - - // Check for the error where we have no config files but - // allow that. If that happens, clear the error. - if errwrap.ContainsType(err, new(config.ErrNoConfigsFound)) && - copts.PathEmptyOk { - log.Printf( - "[WARN] Empty configuration dir, ignoring: %s", copts.Path) - err = nil - mod = module.NewEmptyTree() - } - - if err != nil { - return nil, false, fmt.Errorf("Error loading config: %s", err) - } - } else { - mod = module.NewEmptyTree() - } - - err = mod.Load(m.moduleStorage(m.DataDir()), copts.GetMode) - if err != nil { - return nil, false, fmt.Errorf("Error downloading modules: %s", err) - } - - // Validate the module right away - if err := mod.Validate(); err != nil { - return nil, false, err - } - - opts.Module = mod - opts.Parallelism = copts.Parallelism - opts.State = state.State() - ctx, err := terraform.NewContext(opts) - return ctx, false, err -} - // DataDir returns the directory where local data will be stored. func (m *Meta) DataDir() string { dataDir := DefaultDataDir @@ -248,53 +159,6 @@ func (m *Meta) InputMode() terraform.InputMode { return mode } -// State returns the state for this meta. -func (m *Meta) State() (state.State, error) { - if m.state != nil { - return m.state, nil - } - - result, err := State(m.StateOpts()) - if err != nil { - return nil, err - } - - m.state = result.State - m.stateOutPath = result.StatePath - m.stateResult = result - return m.state, nil -} - -// StateRaw is used to setup the state manually. -func (m *Meta) StateRaw(opts *StateOpts) (*StateResult, error) { - result, err := State(opts) - if err != nil { - return nil, err - } - - m.state = result.State - m.stateOutPath = result.StatePath - m.stateResult = result - return result, nil -} - -// StateOpts returns the default state options -func (m *Meta) StateOpts() *StateOpts { - localPath := m.statePath - if localPath == "" { - localPath = DefaultStateFilename - } - remotePath := filepath.Join(m.DataDir(), DefaultStateFilename) - - return &StateOpts{ - LocalPath: localPath, - LocalPathOut: m.stateOutPath, - RemotePath: remotePath, - RemoteRefresh: true, - BackupPath: m.backupPath, - } -} - // UIInput returns a UIInput object to be used for asking for input. func (m *Meta) UIInput() terraform.UIInput { return &UIInput{ @@ -302,21 +166,6 @@ func (m *Meta) UIInput() terraform.UIInput { } } -// PersistState is used to write out the state, handling backup of -// the existing state file and respecting path configurations. -func (m *Meta) PersistState(s *terraform.State) error { - if err := m.state.WriteState(s); err != nil { - return err - } - - return m.state.PersistState() -} - -// Input returns true if we should ask for input for context. -func (m *Meta) Input() bool { - return !test && m.input && len(m.variables) == 0 -} - // StdinPiped returns true if the input is piped. func (m *Meta) StdinPiped() bool { fi, err := wrappedstreams.Stdin().Stat() @@ -331,11 +180,16 @@ func (m *Meta) StdinPiped() bool { // contextOpts returns the options to use to initialize a Terraform // context with the settings from this Meta. func (m *Meta) contextOpts() *terraform.ContextOpts { - var opts terraform.ContextOpts = *m.ContextOpts + var opts terraform.ContextOpts + if v := m.ContextOpts; v != nil { + opts = *v + } opts.Hooks = []terraform.Hook{m.uiHook(), &terraform.DebugHook{}} - opts.Hooks = append(opts.Hooks, m.ContextOpts.Hooks...) - opts.Hooks = append(opts.Hooks, m.extraHooks...) + if m.ContextOpts != nil { + opts.Hooks = append(opts.Hooks, m.ContextOpts.Hooks...) + } + opts.Hooks = append(opts.Hooks, m.ExtraHooks...) vs := make(map[string]interface{}) for k, v := range opts.Variables { @@ -350,6 +204,7 @@ func (m *Meta) contextOpts() *terraform.ContextOpts { opts.Variables = vs opts.Targets = m.targets opts.UIInput = m.UIInput() + opts.Parallelism = m.parallelism opts.Shadow = m.shadow return &opts @@ -469,6 +324,24 @@ func (m *Meta) uiHook() *UiHook { } } +// confirm asks a yes/no confirmation. +func (m *Meta) confirm(opts *terraform.InputOpts) (bool, error) { + for { + v, err := m.UIInput().Input(opts) + if err != nil { + return false, fmt.Errorf( + "Error asking for confirmation: %s", err) + } + + switch strings.ToLower(v) { + case "no": + return false, nil + case "yes": + return true, nil + } + } +} + const ( // ModuleDepthDefault is the default value for // module depth, which can be overridden by flag @@ -530,28 +403,3 @@ func (m *Meta) outputShadowError(err error, output bool) bool { return true } - -// contextOpts are the options used to load a context from a command. -type contextOpts struct { - // Path to the directory where the root module is. - // - // PathEmptyOk, when set, will allow paths that have no Terraform - // configurations. The result in that case will be an empty module. - Path string - PathEmptyOk bool - - // StatePath is the path to the state file. If this is empty, then - // no state will be loaded. It is also okay for this to be a path to - // a file that doesn't exist; it is assumed that this means that there - // is simply no state. - StatePath string - - // GetMode is the module.GetMode to use when loading the module tree. - GetMode module.GetMode - - // Set to true when running a destroy plan/apply. - Destroy bool - - // Number of concurrent operations allowed - Parallelism int -} diff --git a/command/meta_new.go b/command/meta_new.go new file mode 100644 index 000000000..3138cd2df --- /dev/null +++ b/command/meta_new.go @@ -0,0 +1,101 @@ +package command + +import ( + "fmt" + "os" + "strconv" + + "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/terraform" +) + +// NOTE: Temporary file until this branch is cleaned up. + +// Input returns whether or not input asking is enabled. +func (m *Meta) Input() bool { + if test || !m.input { + return false + } + + if envVar := os.Getenv(InputModeEnvVar); envVar != "" { + if v, err := strconv.ParseBool(envVar); err == nil && !v { + return false + } + } + + return true +} + +// Module loads the module tree for the given root path. +// +// It expects the modules to already be downloaded. This will never +// download any modules. +func (m *Meta) Module(path string) (*module.Tree, error) { + mod, err := module.NewTreeModule("", path) + if err != nil { + // Check for the error where we have no config files + if errwrap.ContainsType(err, new(config.ErrNoConfigsFound)) { + return nil, nil + } + + return nil, err + } + + err = mod.Load(m.moduleStorage(m.DataDir()), module.GetModeNone) + if err != nil { + return nil, fmt.Errorf("Error loading modules: %s", err) + } + + return mod, nil +} + +// Plan returns the plan for the given path. +// +// This only has an effect if the path itself looks like a plan. +// If error is nil and the plan is nil, then the path didn't look like +// a plan. +// +// Error will be non-nil if path looks like a plan and loading the plan +// failed. +func (m *Meta) Plan(path string) (*terraform.Plan, error) { + // Open the path no matter if its a directory or file + f, err := os.Open(path) + defer f.Close() + if err != nil { + return nil, fmt.Errorf( + "Failed to load Terraform configuration or plan: %s", err) + } + + // Stat it so we can check if its a directory + fi, err := f.Stat() + if err != nil { + return nil, fmt.Errorf( + "Failed to load Terraform configuration or plan: %s", err) + } + + // If this path is a directory, then it can't be a plan. Not an error. + if fi.IsDir() { + return nil, nil + } + + // Read the plan + p, err := terraform.ReadPlan(f) + if err != nil { + return nil, err + } + + // We do a validation here that seems odd but if any plan is given, + // we must not have set any extra variables. The plan itself contains + // the variables and those aren't overwritten. + if len(m.variables) > 0 { + return nil, fmt.Errorf( + "You can't set variables with the '-var' or '-var-file' flag\n" + + "when you're applying a plan file. The variables used when\n" + + "the plan was created will be used. If you wish to use different\n" + + "variable values, create a new plan file.") + } + + return p, nil +} diff --git a/command/output.go b/command/output.go index 9ea86db22..44e6932e1 100644 --- a/command/output.go +++ b/command/output.go @@ -20,13 +20,11 @@ func (c *OutputCommand) Run(args []string) int { var module string var jsonOutput bool - cmdFlags := flag.NewFlagSet("output", flag.ContinueOnError) cmdFlags.BoolVar(&jsonOutput, "json", false, "json") cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") cmdFlags.StringVar(&module, "module", "", "module") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } - if err := cmdFlags.Parse(args); err != nil { return 1 } @@ -45,9 +43,17 @@ func (c *OutputCommand) Run(args []string) int { name = args[0] } - stateStore, err := c.Meta.State() + // Load the backend + b, err := c.Backend(nil) if err != nil { - c.Ui.Error(fmt.Sprintf("Error reading state: %s", err)) + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + + // Get the state + stateStore, err := b.State() + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) return 1 } @@ -62,7 +68,6 @@ func (c *OutputCommand) Run(args []string) int { state := stateStore.State() mod := state.ModuleByPath(modPath) - if mod == nil { c.Ui.Error(fmt.Sprintf( "The module %s could not be found. There is nothing to output.", @@ -211,6 +216,7 @@ func formatNestedMap(indent string, outputMap map[string]interface{}) string { return strings.TrimPrefix(outputBuf.String(), "\n") } + func formatMapOutput(indent, outputName string, outputMap map[string]interface{}) string { ks := make([]string, 0, len(outputMap)) for k, _ := range outputMap { diff --git a/command/plan.go b/command/plan.go index 09ce4778a..98b01e151 100644 --- a/command/plan.go +++ b/command/plan.go @@ -1,13 +1,12 @@ package command import ( + "context" "fmt" - "log" - "os" "strings" - "github.com/hashicorp/go-multierror" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/config/module" ) // PlanCommand is a Command implementation that compares a Terraform @@ -30,153 +29,88 @@ func (c *PlanCommand) Run(args []string) int { cmdFlags.StringVar(&outPath, "out", "", "path") cmdFlags.IntVar( &c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism") - cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") + cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path") cmdFlags.BoolVar(&detailed, "detailed-exitcode", false, "detailed-exitcode") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { return 1 } - var path string - args = cmdFlags.Args() - if len(args) > 1 { - c.Ui.Error( - "The plan command expects at most one argument with the path\n" + - "to a Terraform configuration.\n") - cmdFlags.Usage() - return 1 - } else if len(args) == 1 { - path = args[0] - } else { - var err error - path, err = os.Getwd() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) - } - } - - countHook := new(CountHook) - c.Meta.extraHooks = []terraform.Hook{countHook} - - // This is going to keep track of shadow errors - var shadowErr error - - ctx, planned, err := c.Context(contextOpts{ - Destroy: destroy, - Path: path, - StatePath: c.Meta.statePath, - Parallelism: c.Meta.parallelism, - }) + configPath, err := ModulePath(cmdFlags.Args()) if err != nil { c.Ui.Error(err.Error()) return 1 } - if planned { - c.Ui.Output(c.Colorize().Color( - "[reset][bold][yellow]" + - "The plan command received a saved plan file as input. This command\n" + - "will output the saved plan. This will not modify the already-existing\n" + - "plan. If you wish to generate a new plan, please pass in a configuration\n" + - "directory as an argument.\n\n")) + // Check if the path is a plan + plan, err := c.Plan(configPath) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + if plan != nil { // Disable refreshing no matter what since we only want to show the plan refresh = false + + // Set the config path to empty for backend loading + configPath = "" } - err = terraform.SetDebugInfo(DefaultDataDir) + // Load the module if we don't have one yet (not running from plan) + var mod *module.Tree + if plan == nil { + mod, err = c.Module(configPath) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err)) + return 1 + } + } + + // Load the backend + b, err := c.Backend(&BackendOpts{ + ConfigPath: configPath, + Plan: plan, + }) if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + + // Build the operation + opReq := c.Operation() + opReq.Destroy = destroy + opReq.Module = mod + opReq.Plan = plan + opReq.PlanRefresh = refresh + opReq.PlanOutPath = outPath + opReq.Type = backend.OperationTypePlan + + // Perform the operation + op, err := b.Operation(context.Background(), opReq) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error starting operation: %s", err)) + return 1 + } + + // Wait for the operation to complete + <-op.Done() + if err := op.Err; err != nil { c.Ui.Error(err.Error()) return 1 } - if err := ctx.Input(c.InputMode()); err != nil { - c.Ui.Error(fmt.Sprintf("Error configuring: %s", err)) - return 1 - } - - // Record any shadow errors for later - if err := ctx.ShadowError(); err != nil { - shadowErr = multierror.Append(shadowErr, multierror.Prefix( - err, "input operation:")) - } - - if !validateContext(ctx, c.Ui) { - return 1 - } - - if refresh { - c.Ui.Output("Refreshing Terraform state in-memory prior to plan...") - c.Ui.Output("The refreshed state will be used to calculate this plan, but") - c.Ui.Output("will not be persisted to local or remote state storage.\n") - _, err := ctx.Refresh() + /* + err = terraform.SetDebugInfo(DefaultDataDir) if err != nil { - c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err)) + c.Ui.Error(err.Error()) return 1 } - c.Ui.Output("") - } + */ - plan, err := ctx.Plan() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error running plan: %s", err)) - return 1 - } - - if outPath != "" { - log.Printf("[INFO] Writing plan output to: %s", outPath) - f, err := os.Create(outPath) - if err == nil { - defer f.Close() - err = terraform.WritePlan(plan, f) - } - if err != nil { - c.Ui.Error(fmt.Sprintf("Error writing plan file: %s", err)) - return 1 - } - } - - if plan.Diff.Empty() { - c.Ui.Output( - "No changes. Infrastructure is up-to-date. This means that Terraform\n" + - "could not detect any differences between your configuration and\n" + - "the real physical resources that exist. As a result, Terraform\n" + - "doesn't need to do anything.") - return 0 - } - - if outPath == "" { - c.Ui.Output(strings.TrimSpace(planHeaderNoOutput) + "\n") - } else { - c.Ui.Output(fmt.Sprintf( - strings.TrimSpace(planHeaderYesOutput)+"\n", - outPath)) - } - - c.Ui.Output(FormatPlan(&FormatPlanOpts{ - Plan: plan, - Color: c.Colorize(), - ModuleDepth: moduleDepth, - })) - - c.Ui.Output(c.Colorize().Color(fmt.Sprintf( - "[reset][bold]Plan:[reset] "+ - "%d to add, %d to change, %d to destroy.", - countHook.ToAdd+countHook.ToRemoveAndAdd, - countHook.ToChange, - countHook.ToRemove+countHook.ToRemoveAndAdd))) - - // Record any shadow errors for later - if err := ctx.ShadowError(); err != nil { - shadowErr = multierror.Append(shadowErr, multierror.Prefix( - err, "plan operation:")) - } - - // If we have an error in the shadow graph, let the user know. - c.outputShadowError(shadowErr, true) - - if detailed { + if detailed && !op.PlanEmpty { return 2 } + return 0 } @@ -241,28 +175,3 @@ Options: func (c *PlanCommand) Synopsis() string { return "Generate and show an execution plan" } - -const planHeaderNoOutput = ` -The Terraform execution plan has been generated and is shown below. -Resources are shown in alphabetical order for quick scanning. Green resources -will be created (or destroyed and then created if an existing resource -exists), yellow resources are being changed in-place, and red resources -will be destroyed. Cyan entries are data sources to be read. - -Note: You didn't specify an "-out" parameter to save this plan, so when -"apply" is called, Terraform can't guarantee this is what will execute. -` - -const planHeaderYesOutput = ` -The Terraform execution plan has been generated and is shown below. -Resources are shown in alphabetical order for quick scanning. Green resources -will be created (or destroyed and then created if an existing resource -exists), yellow resources are being changed in-place, and red resources -will be destroyed. Cyan entries are data sources to be read. - -Your plan was also saved to the path below. Call the "apply" subcommand -with this plan file and Terraform will exactly execute this execution -plan. - -Path: %s -` diff --git a/command/plan_test.go b/command/plan_test.go index 8e9537e2f..3f0e2cf26 100644 --- a/command/plan_test.go +++ b/command/plan_test.go @@ -5,9 +5,11 @@ import ( "io/ioutil" "os" "path/filepath" + "reflect" "strings" "testing" + "github.com/hashicorp/terraform/helper/copy" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) @@ -239,6 +241,128 @@ func TestPlan_outPathNoChange(t *testing.T) { } } +// When using "-out" with a backend, the plan should encode the backend config +func TestPlan_outBackend(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("plan-out-backend"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Our state + originalState := &terraform.State{ + Modules: []*terraform.ModuleState{ + &terraform.ModuleState{ + Path: []string{"root"}, + Resources: map[string]*terraform.ResourceState{ + "test_instance.foo": &terraform.ResourceState{ + Type: "test_instance", + Primary: &terraform.InstanceState{ + ID: "bar", + }, + }, + }, + }, + }, + } + originalState.Init() + + // Setup our backend state + dataState, srv := testBackendState(t, originalState, 200) + defer srv.Close() + testStateFileRemote(t, dataState) + + outPath := "foo" + p := testProvider() + ui := new(cli.MockUi) + c := &PlanCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{ + "-out", outPath, + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + plan := testReadPlan(t, outPath) + if !plan.Diff.Empty() { + t.Fatalf("Expected empty plan to be written to plan file, got: %s", plan) + } + + if plan.Backend.Empty() { + t.Fatal("should have backend info") + } + if !reflect.DeepEqual(plan.Backend, dataState.Backend) { + t.Fatalf("bad: %#v", plan.Backend) + } +} + +// When using "-out" with a legacy remote state, the plan should encode +// the backend config +func TestPlan_outBackendLegacy(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("plan-out-backend-legacy"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Our state + originalState := &terraform.State{ + Modules: []*terraform.ModuleState{ + &terraform.ModuleState{ + Path: []string{"root"}, + Resources: map[string]*terraform.ResourceState{ + "test_instance.foo": &terraform.ResourceState{ + Type: "test_instance", + Primary: &terraform.InstanceState{ + ID: "bar", + }, + }, + }, + }, + }, + } + originalState.Init() + + // Setup our legacy state + remoteState, srv := testRemoteState(t, originalState, 200) + defer srv.Close() + dataState := terraform.NewState() + dataState.Remote = remoteState + testStateFileRemote(t, dataState) + + outPath := "foo" + p := testProvider() + ui := new(cli.MockUi) + c := &PlanCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{ + "-out", outPath, + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + plan := testReadPlan(t, outPath) + if !plan.Diff.Empty() { + t.Fatalf("Expected empty plan to be written to plan file, got: %s", plan) + } + + if plan.State.Remote.Empty() { + t.Fatal("should have remote info") + } +} + func TestPlan_refresh(t *testing.T) { p := testProvider() ui := new(cli.MockUi) @@ -451,7 +575,7 @@ func TestPlan_validate(t *testing.T) { } actual := ui.ErrorWriter.String() - if !strings.Contains(actual, "can't reference") { + if !strings.Contains(actual, "cannot be computed") { t.Fatalf("bad: %s", actual) } } diff --git a/command/push.go b/command/push.go index db2ae3db4..f3e173ec4 100644 --- a/command/push.go +++ b/command/push.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/atlas-go/archive" "github.com/hashicorp/atlas-go/v1" + "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/terraform" ) @@ -63,59 +64,86 @@ func (c *PushCommand) Run(args []string) int { } } - // The pwd is used for the configuration path if one is not given - pwd, err := os.Getwd() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) - return 1 - } - // Get the path to the configuration depending on the args. - var configPath string - args = cmdFlags.Args() - if len(args) > 1 { - c.Ui.Error("The apply command expects at most one argument.") - cmdFlags.Usage() - return 1 - } else if len(args) == 1 { - configPath = args[0] - } else { - configPath = pwd - } - - // Verify the state is remote, we can't push without a remote state - s, err := c.State() - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err)) - return 1 - } - if !s.State().IsRemote() { - c.Ui.Error( - "Remote state is not enabled. For Atlas to run Terraform\n" + - "for you, remote state must be used and configured. Remote\n" + - "state via any backend is accepted, not just Atlas. To\n" + - "configure remote state, use the `terraform remote config`\n" + - "command.") - return 1 - } - - // Build the context based on the arguments given - ctx, planned, err := c.Context(contextOpts{ - Path: configPath, - StatePath: c.Meta.statePath, - }) - + configPath, err := ModulePath(cmdFlags.Args()) if err != nil { c.Ui.Error(err.Error()) return 1 } - if planned { + + /* + // Verify the state is remote, we can't push without a remote state + s, err := c.State() + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err)) + return 1 + } + if !s.State().IsRemote() { + c.Ui.Error( + "Remote state is not enabled. For Atlas to run Terraform\n" + + "for you, remote state must be used and configured. Remote\n" + + "state via any backend is accepted, not just Atlas. To\n" + + "configure remote state, use the `terraform remote config`\n" + + "command.") + return 1 + } + */ + + // Check if the path is a plan + plan, err := c.Plan(configPath) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + if plan != nil { c.Ui.Error( "A plan file cannot be given as the path to the configuration.\n" + "A path to a module (directory with configuration) must be given.") return 1 } + // Load the module + mod, err := c.Module(configPath) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err)) + return 1 + } + if mod == nil { + c.Ui.Error(fmt.Sprintf( + "No configuration files found in the directory: %s\n\n"+ + "This command requires configuration to run.", + configPath)) + return 1 + } + + // Load the backend + b, err := c.Backend(&BackendOpts{ + ConfigPath: configPath, + }) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + + // We require a local backend + local, ok := b.(backend.Local) + if !ok { + c.Ui.Error(ErrUnsupportedLocalOp) + return 1 + } + + // Build the operation + opReq := c.Operation() + opReq.Module = mod + opReq.Plan = plan + + // Get the context + ctx, _, err := local.Context(opReq) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + // Get the configuration config := ctx.Module().Config() if name == "" { diff --git a/command/refresh.go b/command/refresh.go index 1b0a5cf60..194e95a33 100644 --- a/command/refresh.go +++ b/command/refresh.go @@ -1,12 +1,11 @@ package command import ( + "context" "fmt" - "log" - "os" "strings" - "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/terraform" ) @@ -29,111 +28,50 @@ func (c *RefreshCommand) Run(args []string) int { return 1 } - var configPath string - args = cmdFlags.Args() - if len(args) > 1 { - c.Ui.Error("The refresh command expects at most one argument.") - cmdFlags.Usage() - return 1 - } else if len(args) == 1 { - configPath = args[0] - } else { - var err error - configPath, err = os.Getwd() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) - } - } - - // Check if remote state is enabled - state, err := c.State() - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) - return 1 - } - - // 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. - if !state.State().IsRemote() { - if _, err := os.Stat(c.Meta.statePath); err != nil { - if os.IsNotExist(err) { - c.Ui.Error(fmt.Sprintf( - "The Terraform state file for your infrastructure does not\n"+ - "exist. The 'refresh' command only works and only makes sense\n"+ - "when there is existing state that Terraform is managing. Please\n"+ - "double-check the value given below and try again. If you\n"+ - "haven't created infrastructure with Terraform yet, use the\n"+ - "'terraform apply' command.\n\n"+ - "Path: %s", - c.Meta.statePath)) - return 1 - } - - c.Ui.Error(fmt.Sprintf( - "There was an error reading the Terraform state that is needed\n"+ - "for refreshing. The path and error are shown below.\n\n"+ - "Path: %s\n\nError: %s", - c.Meta.statePath, - err)) - return 1 - } - } - - // This is going to keep track of shadow errors - var shadowErr error - - // Build the context based on the arguments given - ctx, _, err := c.Context(contextOpts{ - Path: configPath, - StatePath: c.Meta.statePath, - Parallelism: c.Meta.parallelism, - }) + configPath, err := ModulePath(cmdFlags.Args()) if err != nil { c.Ui.Error(err.Error()) return 1 } - if err := ctx.Input(c.InputMode()); err != nil { - c.Ui.Error(fmt.Sprintf("Error configuring: %s", err)) - return 1 - } - - // Record any shadow errors for later - if err := ctx.ShadowError(); err != nil { - shadowErr = multierror.Append(shadowErr, multierror.Prefix( - err, "input operation:")) - } - - if !validateContext(ctx, c.Ui) { - return 1 - } - - newState, err := ctx.Refresh() + // Load the module + mod, err := c.Module(configPath) if err != nil { - c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err)) + c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err)) return 1 } - log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath()) - if err := c.Meta.PersistState(newState); err != nil { - c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) + // Load the backend + b, err := c.Backend(&BackendOpts{ConfigPath: configPath}) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) return 1 } - if outputs := outputsAsString(newState, terraform.RootModulePath, ctx.Module().Config().Outputs, true); outputs != "" { + // Build the operation + opReq := c.Operation() + opReq.Type = backend.OperationTypeRefresh + opReq.Module = mod + + // Perform the operation + op, err := b.Operation(context.Background(), opReq) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error starting operation: %s", err)) + return 1 + } + + // Wait for the operation to complete + <-op.Done() + if err := op.Err; err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + // Output the outputs + if outputs := outputsAsString(op.State, terraform.RootModulePath, nil, true); outputs != "" { c.Ui.Output(c.Colorize().Color(outputs)) } - // Record any shadow errors for later - if err := ctx.ShadowError(); err != nil { - shadowErr = multierror.Append(shadowErr, multierror.Prefix( - err, "refresh operation:")) - } - - // If we have an error in the shadow graph, let the user know. - c.outputShadowError(shadowErr, true) - return 0 } diff --git a/command/refresh_test.go b/command/refresh_test.go index 4b8d00660..b5bfe7950 100644 --- a/command/refresh_test.go +++ b/command/refresh_test.go @@ -712,6 +712,10 @@ func TestRefresh_disableBackup(t *testing.T) { if err == nil || !os.IsNotExist(err) { t.Fatalf("backup should not exist") } + _, err = os.Stat("-") + if err == nil || !os.IsNotExist(err) { + t.Fatalf("backup should not exist") + } } func TestRefresh_displaysOutputs(t *testing.T) { diff --git a/command/remote.go b/command/remote.go deleted file mode 100644 index 05d119341..000000000 --- a/command/remote.go +++ /dev/null @@ -1,61 +0,0 @@ -package command - -import ( - "strings" -) - -type RemoteCommand struct { - Meta -} - -func (c *RemoteCommand) Run(argsRaw []string) int { - // Duplicate the args so we can munge them without affecting - // future subcommand invocations which will do the same. - args := make([]string, len(argsRaw)) - copy(args, argsRaw) - args = c.Meta.process(args, false) - - if len(args) == 0 { - c.Ui.Error(c.Help()) - return 1 - } - - switch args[0] { - case "config": - cmd := &RemoteConfigCommand{Meta: c.Meta} - return cmd.Run(args[1:]) - case "pull": - cmd := &RemotePullCommand{Meta: c.Meta} - return cmd.Run(args[1:]) - case "push": - cmd := &RemotePushCommand{Meta: c.Meta} - return cmd.Run(args[1:]) - default: - c.Ui.Error(c.Help()) - return 1 - } -} - -func (c *RemoteCommand) Help() string { - helpText := ` -Usage: terraform remote [options] - - Configure remote state storage with Terraform. - -Options: - - -no-color If specified, output won't contain any color. - -Available subcommands: - - config Configure the remote storage settings. - pull Sync the remote storage by downloading to local storage. - push Sync the remote storage by uploading the local storage. - -` - return strings.TrimSpace(helpText) -} - -func (c *RemoteCommand) Synopsis() string { - return "Configure remote state storage" -} diff --git a/command/remote_config.go b/command/remote_config.go deleted file mode 100644 index 8af803260..000000000 --- a/command/remote_config.go +++ /dev/null @@ -1,385 +0,0 @@ -package command - -import ( - "flag" - "fmt" - "log" - "os" - "strings" - - "github.com/hashicorp/terraform/state" - "github.com/hashicorp/terraform/state/remote" - "github.com/hashicorp/terraform/terraform" -) - -// remoteCommandConfig is used to encapsulate our configuration -type remoteCommandConfig struct { - disableRemote bool - pullOnDisable bool - - statePath string - backupPath string -} - -// RemoteConfigCommand is a Command implementation that is used to -// enable and disable remote state management -type RemoteConfigCommand struct { - Meta - conf remoteCommandConfig - remoteConf *terraform.RemoteState -} - -func (c *RemoteConfigCommand) Run(args []string) int { - // we expect a zero struct value here, but it's not explicitly set in tests - if c.remoteConf == nil { - c.remoteConf = &terraform.RemoteState{} - } - - args = c.Meta.process(args, false) - config := make(map[string]string) - 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", DefaultStateFilename, "path") - cmdFlags.StringVar(&c.conf.backupPath, "backup", "", "path") - cmdFlags.StringVar(&c.remoteConf.Type, "backend", "atlas", "") - cmdFlags.Var((*FlagStringKV)(&config), "backend-config", "config") - cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } - if err := cmdFlags.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("\nError parsing CLI flags: %s", err)) - return 1 - } - - // Lowercase the type - c.remoteConf.Type = strings.ToLower(c.remoteConf.Type) - - // Set the local state path - c.statePath = c.conf.statePath - - // Populate the various configurations - c.remoteConf.Config = config - - // Get the state information. We specifically request the cache only - // for the remote state here because it is possible the remote state - // is invalid and we don't want to error. - stateOpts := c.StateOpts() - stateOpts.RemoteCacheOnly = true - if _, err := c.StateRaw(stateOpts); err != nil { - c.Ui.Error(fmt.Sprintf("Error loading local state: %s", err)) - return 1 - } - - // Get the local and remote [cached] state - localState := c.stateResult.Local.State() - var remoteState *terraform.State - if remote := c.stateResult.Remote; remote != nil { - remoteState = remote.State() - } - - // Check if remote state is being disabled - if c.conf.disableRemote { - if !remoteState.IsRemote() { - c.Ui.Error(fmt.Sprintf("Remote state management not enabled! Aborting.")) - return 1 - } - if !localState.Empty() { - 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, and then do the correct operation - var result int - haveCache := !remoteState.Empty() - haveLocal := !localState.Empty() - switch { - case haveCache && haveLocal: - c.Ui.Error(fmt.Sprintf("Remote state is enabled, but non-managed state file '%s' is also present!", - c.conf.statePath)) - result = 1 - - case !haveCache && !haveLocal: - // If we don't have either state file, initialize a blank state file - result = c.initBlankState() - - case haveCache && !haveLocal: - // Update the remote state target potentially - result = c.updateRemoteConfig() - - case !haveCache && haveLocal: - // Enable remote state management - result = c.enableRemoteState() - } - - // If there was an error, return right away - if result != 0 { - return result - } - - // If we're not pulling, then do nothing - if !c.conf.pullOnDisable { - return result - } - - // Otherwise, refresh the state - stateResult, err := c.StateRaw(c.StateOpts()) - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error while performing the initial pull. The error message is shown\n"+ - "below. Note that remote state was properly configured, so you don't\n"+ - "need to reconfigure. You can now use `push` and `pull` directly.\n"+ - "\n%s", err)) - return 1 - } - - state := stateResult.State - if err := state.RefreshState(); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error while performing the initial pull. The error message is shown\n"+ - "below. Note that remote state was properly configured, so you don't\n"+ - "need to reconfigure. You can now use `push` and `pull` directly.\n"+ - "\n%s", err)) - return 1 - } - - c.Ui.Output(c.Colorize().Color(fmt.Sprintf( - "[reset][bold][green]Remote state configured and pulled."))) - return 0 -} - -// disableRemoteState is used to disable remote state management, -// and move the state file into place. -func (c *RemoteConfigCommand) disableRemoteState() int { - if c.stateResult == nil { - c.Ui.Error(fmt.Sprintf( - "Internal error. State() must be called internally before remote\n" + - "state can be disabled. Please report this as a bug.")) - return 1 - } - if !c.stateResult.State.State().IsRemote() { - c.Ui.Error(fmt.Sprintf( - "Remote state is not enabled. Can't disable remote state.")) - return 1 - } - local := c.stateResult.Local - remote := c.stateResult.Remote - - // Ensure we have the latest state before disabling - if c.conf.pullOnDisable { - log.Printf("[INFO] Refreshing local state from remote server") - if err := remote.RefreshState(); err != nil { - c.Ui.Error(fmt.Sprintf( - "Failed to refresh from remote state: %s", err)) - return 1 - } - - // Exit if we were unable to update - if change := remote.RefreshResult(); !change.SuccessfulPull() { - c.Ui.Error(fmt.Sprintf("%s", change)) - return 1 - } else { - log.Printf("[INFO] %s", change) - } - } - - // Clear the remote management, and copy into place - newState := remote.State() - newState.Remote = nil - if err := local.WriteState(newState); err != nil { - c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %s", - c.conf.statePath, err)) - return 1 - } - if err := local.PersistState(); err != nil { - c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %s", - c.conf.statePath, err)) - return 1 - } - - // Remove the old state file - if err := os.Remove(c.stateResult.RemotePath); 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 *RemoteConfigCommand) validateRemoteConfig() error { - conf := c.remoteConf - _, err := remote.NewClient(conf.Type, conf.Config) - if err != nil { - c.Ui.Error(fmt.Sprintf( - "%s\n\n"+ - "If the error message above mentions requiring or modifying configuration\n"+ - "options, these are set using the `-backend-config` flag. Example:\n"+ - "-backend-config=\"name=foo\" to set the `name` configuration", - err)) - } - return err -} - -// initBlank state is used to initialize a blank state that is -// remote enabled -func (c *RemoteConfigCommand) initBlankState() int { - // Validate the remote configuration - if err := c.validateRemoteConfig(); err != nil { - return 1 - } - - // Make a blank state, attach the remote configuration - blank := terraform.NewState() - blank.Remote = c.remoteConf - - // Persist the state - remote := &state.LocalState{Path: c.stateResult.RemotePath} - if err := remote.WriteState(blank); err != nil { - c.Ui.Error(fmt.Sprintf("Failed to initialize state file: %v", err)) - return 1 - } - if err := remote.PersistState(); 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 *RemoteConfigCommand) updateRemoteConfig() int { - // Validate the remote configuration - if err := c.validateRemoteConfig(); err != nil { - return 1 - } - - // Read in the local state, which is just the cache of the remote state - remote := c.stateResult.Remote.Cache - - // Update the configuration - state := remote.State() - state.Remote = c.remoteConf - if err := remote.WriteState(state); err != nil { - c.Ui.Error(fmt.Sprintf("%s", err)) - return 1 - } - if err := remote.PersistState(); 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 *RemoteConfigCommand) enableRemoteState() int { - // Validate the remote configuration - if err := c.validateRemoteConfig(); err != nil { - return 1 - } - - // Read the local state - local := c.stateResult.Local - if err := local.RefreshState(); err != nil { - c.Ui.Error(fmt.Sprintf("Failed to read local state: %s", 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 + DefaultBackupExtension - } - - log.Printf("[INFO] Writing backup state to: %s", backupPath) - backup := &state.LocalState{Path: backupPath} - if err := backup.WriteState(local.State()); err != nil { - c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err)) - return 1 - } - if err := backup.PersistState(); err != nil { - c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err)) - return 1 - } - } - - // Update the local configuration, move into place - state := local.State() - state.Remote = c.remoteConf - remote := c.stateResult.Remote - if err := remote.WriteState(state); err != nil { - c.Ui.Error(fmt.Sprintf("%s", err)) - return 1 - } - if err := remote.PersistState(); err != nil { - c.Ui.Error(fmt.Sprintf("%s", err)) - return 1 - } - - // Remove the original, local 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 -} - -func (c *RemoteConfigCommand) Help() string { - helpText := ` -Usage: terraform remote config [options] - - Configures Terraform to use a remote state server. This allows state - to be pulled down when necessary and then pushed to the server when - updated. In this mode, the state file does not need to be stored durably - since the remote server provides the durability. - -Options: - - -backend=Atlas Specifies the type of remote backend. Must be one - of Atlas, Consul, Etcd, GCS, HTTP, MAS, S3, or Swift. - Defaults to Atlas. - - -backend-config="k=v" Specifies configuration for the remote storage - backend. This can be specified multiple times. - - -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 If disabling, this controls if the remote state is - pulled before disabling. If enabling, this controls - if the remote state is pulled after enabling. This - defaults to true. - - -state=path Path to read state. Defaults to "terraform.tfstate" - unless remote state is enabled. - - -no-color If specified, output won't contain any color. - -` - return strings.TrimSpace(helpText) -} - -func (c *RemoteConfigCommand) Synopsis() string { - return "Configures remote state management" -} diff --git a/command/remote_config_test.go b/command/remote_config_test.go deleted file mode 100644 index 77f4d471a..000000000 --- a/command/remote_config_test.go +++ /dev/null @@ -1,449 +0,0 @@ -package command - -import ( - "bytes" - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/hashicorp/terraform/state" - "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/cli" -) - -// Test disabling remote management -func TestRemoteConfig_disable(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - // Create remote state file, this should be pulled - s := terraform.NewState() - s.Serial = 10 - conf, srv := testRemoteState(t, s, 200) - defer srv.Close() - - // Persist local remote state - s = terraform.NewState() - s.Serial = 5 - s.Remote = conf - - // Write the state - statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename) - state := &state.LocalState{Path: statePath} - if err := state.WriteState(s); err != nil { - t.Fatalf("err: %s", err) - } - if err := state.PersistState(); err != nil { - t.Fatalf("err: %s", err) - } - - ui := new(cli.MockUi) - c := &RemoteConfigCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, - }, - } - args := []string{"-disable"} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) - } - - // Local state file should be removed and the local cache should exist - testRemoteLocal(t, true) - testRemoteLocalCache(t, false) - - // Check that the state file was updated - raw, _ := ioutil.ReadFile(DefaultStateFilename) - newState, err := terraform.ReadState(bytes.NewReader(raw)) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Ensure we updated - if newState.Remote != nil { - t.Fatalf("remote configuration not removed") - } -} - -// Test disabling remote management without pulling -func TestRemoteConfig_disable_noPull(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - // Create remote state file, this should be pulled - s := terraform.NewState() - s.Serial = 10 - conf, srv := testRemoteState(t, s, 200) - defer srv.Close() - - // Persist local remote state - s = terraform.NewState() - s.Serial = 5 - s.Remote = conf - - // Write the state - statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename) - state := &state.LocalState{Path: statePath} - if err := state.WriteState(s); err != nil { - t.Fatalf("err: %s", err) - } - if err := state.PersistState(); err != nil { - t.Fatalf("err: %s", err) - } - - ui := new(cli.MockUi) - c := &RemoteConfigCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, - }, - } - args := []string{"-disable", "-pull=false"} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) - } - - // Local state file should be removed and the local cache should exist - testRemoteLocal(t, true) - testRemoteLocalCache(t, false) - - // Check that the state file was updated - raw, _ := ioutil.ReadFile(DefaultStateFilename) - newState, err := terraform.ReadState(bytes.NewReader(raw)) - if err != nil { - t.Fatalf("err: %v", err) - } - - if newState.Remote != nil { - t.Fatalf("remote configuration not removed") - } -} - -// Test disabling remote management when not enabled -func TestRemoteConfig_disable_notEnabled(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - ui := new(cli.MockUi) - c := &RemoteConfigCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, - }, - } - - args := []string{"-disable"} - if code := c.Run(args); code != 1 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) - } -} - -// Test disabling remote management with a state file in the way -func TestRemoteConfig_disable_otherState(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - // Persist local remote state - s := terraform.NewState() - s.Serial = 5 - - // Write the state - statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename) - state := &state.LocalState{Path: statePath} - if err := state.WriteState(s); err != nil { - t.Fatalf("err: %s", err) - } - if err := state.PersistState(); err != nil { - t.Fatalf("err: %s", err) - } - - // Also put a file at the default path - fh, err := os.Create(DefaultStateFilename) - if err != nil { - t.Fatalf("err: %v", err) - } - err = terraform.WriteState(s, fh) - fh.Close() - if err != nil { - t.Fatalf("err: %v", err) - } - - ui := new(cli.MockUi) - c := &RemoteConfigCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, - }, - } - - args := []string{"-disable"} - if code := c.Run(args); code != 1 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) - } -} - -// Test the case where both managed and non managed state present -func TestRemoteConfig_managedAndNonManaged(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - // Persist local remote state - s := terraform.NewState() - s.Serial = 5 - - // Write the state - statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename) - state := &state.LocalState{Path: statePath} - if err := state.WriteState(s); err != nil { - t.Fatalf("err: %s", err) - } - if err := state.PersistState(); err != nil { - t.Fatalf("err: %s", err) - } - - // Also put a file at the default path - fh, err := os.Create(DefaultStateFilename) - if err != nil { - t.Fatalf("err: %v", err) - } - err = terraform.WriteState(s, fh) - fh.Close() - if err != nil { - t.Fatalf("err: %v", err) - } - - ui := new(cli.MockUi) - c := &RemoteConfigCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, - }, - } - - args := []string{} - if code := c.Run(args); code != 1 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) - } -} - -// Test initializing blank state -func TestRemoteConfig_initBlank(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - ui := new(cli.MockUi) - c := &RemoteConfigCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, - }, - } - - args := []string{ - "-backend=http", - "-backend-config", "address=http://example.com", - "-backend-config", "access_token=test", - "-pull=false", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) - } - - remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename) - ls := &state.LocalState{Path: remotePath} - if err := ls.RefreshState(); err != nil { - t.Fatalf("err: %s", err) - } - - local := ls.State() - if local.Remote.Type != "http" { - t.Fatalf("Bad: %#v", local.Remote) - } - if local.Remote.Config["address"] != "http://example.com" { - t.Fatalf("Bad: %#v", local.Remote) - } - if local.Remote.Config["access_token"] != "test" { - t.Fatalf("Bad: %#v", local.Remote) - } -} - -// Test initializing without remote settings -func TestRemoteConfig_initBlank_missingRemote(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - ui := new(cli.MockUi) - c := &RemoteConfigCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, - }, - } - - args := []string{} - if code := c.Run(args); code != 1 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) - } -} - -// Test updating remote config -func TestRemoteConfig_updateRemote(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - // Persist local remote state - s := terraform.NewState() - s.Serial = 5 - s.Remote = &terraform.RemoteState{ - Type: "invalid", - } - - // Write the state - statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename) - ls := &state.LocalState{Path: statePath} - if err := ls.WriteState(s); err != nil { - t.Fatalf("err: %s", err) - } - if err := ls.PersistState(); err != nil { - t.Fatalf("err: %s", err) - } - - ui := new(cli.MockUi) - c := &RemoteConfigCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, - }, - } - - args := []string{ - "-backend=http", - "-backend-config", "address=http://example.com", - "-backend-config", "access_token=test", - "-pull=false", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) - } - - remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename) - ls = &state.LocalState{Path: remotePath} - if err := ls.RefreshState(); err != nil { - t.Fatalf("err: %s", err) - } - local := ls.State() - - if local.Remote.Type != "http" { - t.Fatalf("Bad: %#v", local.Remote) - } - if local.Remote.Config["address"] != "http://example.com" { - t.Fatalf("Bad: %#v", local.Remote) - } - if local.Remote.Config["access_token"] != "test" { - t.Fatalf("Bad: %#v", local.Remote) - } -} - -// Test enabling remote state -func TestRemoteConfig_enableRemote(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - // Create a non-remote enabled state - s := terraform.NewState() - s.Serial = 5 - - // Add the state at the default path - fh, err := os.Create(DefaultStateFilename) - if err != nil { - t.Fatalf("err: %v", err) - } - err = terraform.WriteState(s, fh) - fh.Close() - if err != nil { - t.Fatalf("err: %v", err) - } - - ui := new(cli.MockUi) - c := &RemoteConfigCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, - }, - } - - args := []string{ - "-backend=http", - "-backend-config", "address=http://example.com", - "-backend-config", "access_token=test", - "-pull=false", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) - } - - remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename) - ls := &state.LocalState{Path: remotePath} - if err := ls.RefreshState(); err != nil { - t.Fatalf("err: %s", err) - } - local := ls.State() - - if local.Remote.Type != "http" { - t.Fatalf("Bad: %#v", local.Remote) - } - if local.Remote.Config["address"] != "http://example.com" { - t.Fatalf("Bad: %#v", local.Remote) - } - if local.Remote.Config["access_token"] != "test" { - t.Fatalf("Bad: %#v", local.Remote) - } - - // Backup file should exist, state file should not - testRemoteLocal(t, false) - testRemoteLocalBackup(t, true) -} - -func testRemoteLocal(t *testing.T, exists bool) { - _, err := os.Stat(DefaultStateFilename) - if os.IsNotExist(err) && !exists { - return - } - if err == nil && exists { - return - } - - t.Fatalf("bad: %#v", err) -} - -func testRemoteLocalBackup(t *testing.T, exists bool) { - _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension) - if os.IsNotExist(err) && !exists { - return - } - if err == nil && exists { - return - } - if err == nil && !exists { - t.Fatal("expected local backup to exist") - } - - t.Fatalf("bad: %#v", err) -} - -func testRemoteLocalCache(t *testing.T, exists bool) { - _, err := os.Stat(filepath.Join(DefaultDataDir, DefaultStateFilename)) - if os.IsNotExist(err) && !exists { - return - } - if err == nil && exists { - return - } - if err == nil && !exists { - t.Fatal("expected local cache to exist") - } - - t.Fatalf("bad: %#v", err) -} diff --git a/command/remote_pull.go b/command/remote_pull.go deleted file mode 100644 index b159d5a3b..000000000 --- a/command/remote_pull.go +++ /dev/null @@ -1,86 +0,0 @@ -package command - -import ( - "flag" - "fmt" - "strings" - - "github.com/hashicorp/terraform/state" -) - -type RemotePullCommand struct { - Meta -} - -func (c *RemotePullCommand) Run(args []string) int { - args = c.Meta.process(args, false) - cmdFlags := flag.NewFlagSet("pull", flag.ContinueOnError) - cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } - if err := cmdFlags.Parse(args); err != nil { - return 1 - } - - // Read out our state - s, err := c.State() - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err)) - return 1 - } - localState := s.State() - - // If remote state isn't enabled, it is a problem. - if !localState.IsRemote() { - c.Ui.Error("Remote state not enabled!") - return 1 - } - - // We need the CacheState structure in order to do anything - var cache *state.CacheState - if bs, ok := s.(*state.BackupState); ok { - if cs, ok := bs.Real.(*state.CacheState); ok { - cache = cs - } - } - if cache == nil { - c.Ui.Error(fmt.Sprintf( - "Failed to extract internal CacheState from remote state.\n" + - "This is an internal error, please report it as a bug.")) - return 1 - } - - // Refresh the state - if err := cache.RefreshState(); err != nil { - c.Ui.Error(fmt.Sprintf( - "Failed to refresh from remote state: %s", err)) - return 1 - } - - // Use an error exit code if the update was not a success - change := cache.RefreshResult() - if !change.SuccessfulPull() { - c.Ui.Error(fmt.Sprintf("%s", change)) - return 1 - } else { - c.Ui.Output(c.Colorize().Color(fmt.Sprintf( - "[reset][bold][green]%s", change))) - } - - return 0 -} - -func (c *RemotePullCommand) Help() string { - helpText := ` -Usage: terraform pull [options] - - Refreshes the cached state file from the remote server. - -Options: - - -no-color If specified, output won't contain any color. -` - return strings.TrimSpace(helpText) -} - -func (c *RemotePullCommand) Synopsis() string { - return "Refreshes the local state copy from the remote server" -} diff --git a/command/remote_pull_test.go b/command/remote_pull_test.go deleted file mode 100644 index a867877e1..000000000 --- a/command/remote_pull_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package command - -import ( - "bytes" - "crypto/md5" - "encoding/base64" - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "testing" - - "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/cli" -) - -func TestRemotePull_noRemote(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - ui := new(cli.MockUi) - c := &RemotePullCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, - }, - } - - args := []string{} - if code := c.Run(args); code != 1 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) - } -} - -func TestRemotePull_local(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - s := terraform.NewState() - s.Serial = 10 - conf, srv := testRemoteState(t, s, 200) - - s = terraform.NewState() - s.Serial = 5 - s.Remote = conf - defer srv.Close() - - // Store the local state - statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename) - if err := os.MkdirAll(filepath.Dir(statePath), 0755); err != nil { - t.Fatalf("err: %s", err) - } - f, err := os.Create(statePath) - if err != nil { - t.Fatalf("err: %s", err) - } - err = terraform.WriteState(s, f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - ui := new(cli.MockUi) - c := &RemotePullCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, - }, - } - args := []string{} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) - } -} - -// testRemoteState is used to make a test HTTP server to -// return a given state file -func testRemoteState(t *testing.T, s *terraform.State, c int) (*terraform.RemoteState, *httptest.Server) { - var b64md5 string - buf := bytes.NewBuffer(nil) - - cb := func(resp http.ResponseWriter, req *http.Request) { - if req.Method == "PUT" { - resp.WriteHeader(c) - return - } - if s == nil { - resp.WriteHeader(404) - return - } - - resp.Header().Set("Content-MD5", b64md5) - resp.Write(buf.Bytes()) - } - - srv := httptest.NewServer(http.HandlerFunc(cb)) - remote := &terraform.RemoteState{ - Type: "http", - Config: map[string]string{"address": srv.URL}, - } - - if s != nil { - // Set the remote data - s.Remote = remote - - enc := json.NewEncoder(buf) - if err := enc.Encode(s); err != nil { - t.Fatalf("err: %v", err) - } - md5 := md5.Sum(buf.Bytes()) - b64md5 = base64.StdEncoding.EncodeToString(md5[:16]) - } - - return remote, srv -} diff --git a/command/remote_push.go b/command/remote_push.go deleted file mode 100644 index 97ff8f951..000000000 --- a/command/remote_push.go +++ /dev/null @@ -1,96 +0,0 @@ -package command - -import ( - "flag" - "fmt" - "strings" - - "github.com/hashicorp/terraform/state" -) - -type RemotePushCommand struct { - Meta -} - -func (c *RemotePushCommand) Run(args []string) int { - var force bool - args = c.Meta.process(args, false) - cmdFlags := flag.NewFlagSet("push", flag.ContinueOnError) - cmdFlags.BoolVar(&force, "force", false, "") - cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } - if err := cmdFlags.Parse(args); err != nil { - return 1 - } - - // Read out our state - s, err := c.State() - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err)) - return 1 - } - localState := s.State() - - // If remote state isn't enabled, it is a problem. - if !localState.IsRemote() { - c.Ui.Error("Remote state not enabled!") - return 1 - } - - // We need the CacheState structure in order to do anything - var cache *state.CacheState - if bs, ok := s.(*state.BackupState); ok { - if cs, ok := bs.Real.(*state.CacheState); ok { - cache = cs - } - } - if cache == nil { - c.Ui.Error(fmt.Sprintf( - "Failed to extract internal CacheState from remote state.\n" + - "This is an internal error, please report it as a bug.")) - return 1 - } - - // Refresh the cache state - if err := cache.Cache.RefreshState(); err != nil { - c.Ui.Error(fmt.Sprintf( - "Failed to refresh from remote state: %s", err)) - return 1 - } - - // Write it to the real storage - remote := cache.Durable - if err := remote.WriteState(cache.Cache.State()); err != nil { - c.Ui.Error(fmt.Sprintf("Error writing state: %s", err)) - return 1 - } - if err := remote.PersistState(); err != nil { - c.Ui.Error(fmt.Sprintf("Error saving state: %s", err)) - return 1 - } - - c.Ui.Output(c.Colorize().Color( - "[reset][bold][green]State successfully pushed!")) - return 0 -} - -func (c *RemotePushCommand) Help() string { - helpText := ` -Usage: terraform push [options] - - Uploads the latest state to the remote server. - -Options: - - -no-color If specified, output won't contain any color. - - -force Forces the upload of the local state, ignoring any - conflicts. This should be used carefully, as force pushing - can cause remote state information to be lost. - -` - return strings.TrimSpace(helpText) -} - -func (c *RemotePushCommand) Synopsis() string { - return "Uploads the local state to the remote server" -} diff --git a/command/remote_push_test.go b/command/remote_push_test.go deleted file mode 100644 index d92c3e8ab..000000000 --- a/command/remote_push_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package command - -import ( - "os" - "path/filepath" - "testing" - - "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/cli" -) - -func TestRemotePush_noRemote(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - ui := new(cli.MockUi) - c := &RemotePushCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, - }, - } - - args := []string{} - if code := c.Run(args); code != 1 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) - } -} - -func TestRemotePush_local(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - s := terraform.NewState() - s.Serial = 5 - conf, srv := testRemoteState(t, s, 200) - defer srv.Close() - - s = terraform.NewState() - s.Serial = 10 - s.Remote = conf - - // Store the local state - statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename) - if err := os.MkdirAll(filepath.Dir(statePath), 0755); err != nil { - t.Fatalf("err: %s", err) - } - f, err := os.Create(statePath) - if err != nil { - t.Fatalf("err: %s", err) - } - err = terraform.WriteState(s, f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } - - ui := new(cli.MockUi) - c := &RemotePushCommand{ - Meta: Meta{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, - }, - } - args := []string{} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) - } -} diff --git a/command/show.go b/command/show.go index 8a32c4a8d..07afae053 100644 --- a/command/show.go +++ b/command/show.go @@ -6,6 +6,7 @@ import ( "os" "strings" + "github.com/hashicorp/terraform/command/format" "github.com/hashicorp/terraform/terraform" ) @@ -66,14 +67,26 @@ func (c *ShowCommand) Run(args []string) int { } } } else { - stateOpts := c.StateOpts() - stateOpts.RemoteCacheOnly = true - result, err := State(stateOpts) + // Load the backend + b, err := c.Backend(nil) if err != nil { - c.Ui.Error(fmt.Sprintf("Error reading state: %s", err)) + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) return 1 } - state = result.State.State() + + // Get the state + stateStore, err := b.State() + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) + return 1 + } + + if err := stateStore.RefreshState(); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) + return 1 + } + + state = stateStore.State() if state == nil { c.Ui.Output("No state.") return 0 @@ -92,7 +105,7 @@ func (c *ShowCommand) Run(args []string) int { } if plan != nil { - c.Ui.Output(FormatPlan(&FormatPlanOpts{ + c.Ui.Output(format.Plan(&format.PlanOpts{ Plan: plan, Color: c.Colorize(), ModuleDepth: moduleDepth, @@ -100,7 +113,7 @@ func (c *ShowCommand) Run(args []string) int { return 0 } - c.Ui.Output(FormatState(&FormatStateOpts{ + c.Ui.Output(format.State(&format.StateOpts{ State: state, Color: c.Colorize(), ModuleDepth: moduleDepth, diff --git a/command/show_test.go b/command/show_test.go index eb73ebe2f..ad5c52f88 100644 --- a/command/show_test.go +++ b/command/show_test.go @@ -129,20 +129,11 @@ func TestShow_noArgsRemoteState(t *testing.T) { tmp, cwd := testCwd(t) defer testFixCwd(t, tmp, cwd) - // Pretend like we have a local cache of remote state - remoteStatePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename) - if err := os.MkdirAll(filepath.Dir(remoteStatePath), 0755); err != nil { - t.Fatalf("err: %s", err) - } - f, err := os.Create(remoteStatePath) - if err != nil { - t.Fatalf("err: %s", err) - } - err = terraform.WriteState(testState(), f) - f.Close() - if err != nil { - t.Fatalf("err: %s", err) - } + // Create some legacy remote state + legacyState := testState() + _, srv := testRemoteState(t, legacyState, 200) + defer srv.Close() + testStateFileRemote(t, legacyState) ui := new(cli.MockUi) c := &ShowCommand{ diff --git a/command/state_list.go b/command/state_list.go index daa96b684..d66531b1e 100644 --- a/command/state_list.go +++ b/command/state_list.go @@ -24,10 +24,18 @@ func (c *StateListCommand) Run(args []string) int { } args = cmdFlags.Args() - state, err := c.State() + // Load the backend + b, err := c.Backend(nil) if err != nil { - c.Ui.Error(fmt.Sprintf(errStateLoadingState, err)) - return cli.RunResultHelp + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + + // Get the state + state, err := b.State() + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) + return 1 } stateReal := state.State() diff --git a/command/state_meta.go b/command/state_meta.go index 760119302..5a136d4d8 100644 --- a/command/state_meta.go +++ b/command/state_meta.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + backendlocal "github.com/hashicorp/terraform/backend/local" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" ) @@ -19,17 +20,31 @@ func (c *StateMeta) State(m *Meta) (state.State, error) { // Disable backups since we wrap it manually below m.backupPath = "-" - // Get the state (shouldn't be wrapped in a backup) - s, err := m.State() + // Load the backend + b, err := m.Backend(nil) if err != nil { return nil, err } + // Get the state + s, err := b.State() + if err != nil { + return nil, err + } + + // Get a local backend + localRaw, err := m.Backend(&BackendOpts{ForceLocal: true}) + if err != nil { + // This should never fail + panic(err) + } + localB := localRaw.(*backendlocal.Local) + // Determine the backup path. stateOutPath is set to the resulting // file where state is written (cached in the case of remote state) backupPath := fmt.Sprintf( "%s.%d%s", - m.stateOutPath, + localB.StateOutPath, time.Now().UTC().Unix(), DefaultBackupExtension) diff --git a/command/state_pull.go b/command/state_pull.go new file mode 100644 index 000000000..73059c6cc --- /dev/null +++ b/command/state_pull.go @@ -0,0 +1,71 @@ +package command + +import ( + "bytes" + "fmt" + "strings" + + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" +) + +// StatePullCommand is a Command implementation that shows a single resource. +type StatePullCommand struct { + Meta + StateMeta +} + +func (c *StatePullCommand) Run(args []string) int { + args = c.Meta.process(args, true) + + cmdFlags := c.Meta.flagSet("state pull") + if err := cmdFlags.Parse(args); err != nil { + return cli.RunResultHelp + } + args = cmdFlags.Args() + + // Load the backend + b, err := c.Backend(nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + + // Get the state + state, err := b.State() + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) + return 1 + } + if err := state.RefreshState(); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) + return 1 + } + + var buf bytes.Buffer + if err := terraform.WriteState(state.State(), &buf); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) + return 1 + } + + c.Ui.Output(buf.String()) + return 0 +} + +func (c *StatePullCommand) Help() string { + helpText := ` +Usage: terraform state pull [options] + + Pull the state from its location and output it to stdout. + + This command "pulls" the current state and outputs it to stdout. + The primary use of this is for state stored remotely. This command + will still work with local state but is less useful for this. + +` + return strings.TrimSpace(helpText) +} + +func (c *StatePullCommand) Synopsis() string { + return "Pull current state and output to stdout" +} diff --git a/command/state_pull_test.go b/command/state_pull_test.go new file mode 100644 index 000000000..3176a4c54 --- /dev/null +++ b/command/state_pull_test.go @@ -0,0 +1,39 @@ +package command + +import ( + "strings" + "testing" + + "github.com/mitchellh/cli" +) + +func TestStatePull(t *testing.T) { + tmp, cwd := testCwd(t) + defer testFixCwd(t, tmp, cwd) + + // Create some legacy remote state + legacyState := testState() + _, srv := testRemoteState(t, legacyState, 200) + defer srv.Close() + testStateFileRemote(t, legacyState) + + p := testProvider() + ui := new(cli.MockUi) + c := &StatePullCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + expected := "test_instance.foo" + actual := ui.OutputWriter.String() + if !strings.Contains(actual, expected) { + t.Fatalf("expected:\n%s\n\nto include: %q", actual, expected) + } +} diff --git a/command/state_push.go b/command/state_push.go new file mode 100644 index 000000000..a2b130f30 --- /dev/null +++ b/command/state_push.go @@ -0,0 +1,144 @@ +package command + +import ( + "fmt" + "os" + "strings" + + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" +) + +// StatePushCommand is a Command implementation that shows a single resource. +type StatePushCommand struct { + Meta + StateMeta +} + +func (c *StatePushCommand) Run(args []string) int { + args = c.Meta.process(args, true) + + var flagForce bool + cmdFlags := c.Meta.flagSet("state push") + cmdFlags.BoolVar(&flagForce, "force", false, "") + if err := cmdFlags.Parse(args); err != nil { + return cli.RunResultHelp + } + args = cmdFlags.Args() + + if len(args) != 1 { + c.Ui.Error("Exactly one argument expected: path to state to push") + return 1 + } + + // Read the state + f, err := os.Open(args[0]) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + sourceState, err := terraform.ReadState(f) + f.Close() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error reading source state %q: %s", args[0], err)) + return 1 + } + + // Load the backend + b, err := c.Backend(nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + + // Get the state + state, err := b.State() + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err)) + return 1 + } + if err := state.RefreshState(); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err)) + return 1 + } + dstState := state.State() + + // If we're not forcing, then perform safety checks + if !flagForce && !dstState.Empty() { + if !dstState.SameLineage(sourceState) { + c.Ui.Error(strings.TrimSpace(errStatePushLineage)) + return 1 + } + + age, err := dstState.CompareAges(sourceState) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + if age == terraform.StateAgeReceiverNewer { + c.Ui.Error(strings.TrimSpace(errStatePushSerialNewer)) + return 1 + } + } + + // Overwrite it + if err := state.WriteState(sourceState); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err)) + return 1 + } + if err := state.PersistState(); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err)) + return 1 + } + + return 0 +} + +func (c *StatePushCommand) Help() string { + helpText := ` +Usage: terraform state push [options] PATH + + Update remote state from a local state file at PATH. + + This command "pushes" a local state and overwrites remote state + with a local state file. The command will protect you against writing + an older serial or a different state file lineage unless you specify the + "-force" flag. + + This command works with local state (it will overwrite the local + state), but is less useful for this use case. + +Options: + + -force Write the state even if lineages don't match or the + remote serial is higher. + +` + return strings.TrimSpace(helpText) +} + +func (c *StatePushCommand) Synopsis() string { + return "Update remote state from a local state file" +} + +const errStatePushLineage = ` +The lineages do not match! The state will not be pushed. + +The "lineage" is a unique identifier given to a state on creation. It helps +protect Terraform from overwriting a seemingly unrelated state file since it +represents potentially losing real state. + +Please verify you're pushing the correct state. If you're sure you are, you +can force the behavior with the "-force" flag. +` + +const errStatePushSerialNewer = ` +The destination state has a higher serial number! The state will not be pushed. + +A higher serial could indicate that there is data in the destination state +that was not present when the source state was created. As a protection measure, +Terraform will not automatically overwrite this state. + +Please verify you're pushing the correct state. If you're sure you are, you +can force the behavior with the "-force" flag. +` diff --git a/command/state_push_test.go b/command/state_push_test.go new file mode 100644 index 000000000..d63b193f6 --- /dev/null +++ b/command/state_push_test.go @@ -0,0 +1,154 @@ +package command + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform/helper/copy" + "github.com/mitchellh/cli" +) + +func TestStatePush_empty(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("state-push-good"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + expected := testStateRead(t, "replace.tfstate") + + p := testProvider() + ui := new(cli.MockUi) + c := &StatePushCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{"replace.tfstate"} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + actual := testStateRead(t, "local-state.tfstate") + if !actual.Equal(expected) { + t.Fatalf("bad: %#v", actual) + } +} + +func TestStatePush_replaceMatch(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("state-push-replace-match"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + expected := testStateRead(t, "replace.tfstate") + + p := testProvider() + ui := new(cli.MockUi) + c := &StatePushCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{"replace.tfstate"} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + actual := testStateRead(t, "local-state.tfstate") + if !actual.Equal(expected) { + t.Fatalf("bad: %#v", actual) + } +} + +func TestStatePush_lineageMismatch(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("state-push-bad-lineage"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + expected := testStateRead(t, "local-state.tfstate") + + p := testProvider() + ui := new(cli.MockUi) + c := &StatePushCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{"replace.tfstate"} + if code := c.Run(args); code != 1 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + actual := testStateRead(t, "local-state.tfstate") + if !actual.Equal(expected) { + t.Fatalf("bad: %#v", actual) + } +} + +func TestStatePush_serialNewer(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("state-push-serial-newer"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + expected := testStateRead(t, "local-state.tfstate") + + p := testProvider() + ui := new(cli.MockUi) + c := &StatePushCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{"replace.tfstate"} + if code := c.Run(args); code != 1 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + actual := testStateRead(t, "local-state.tfstate") + if !actual.Equal(expected) { + t.Fatalf("bad: %#v", actual) + } +} + +func TestStatePush_serialOlder(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("state-push-serial-older"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + expected := testStateRead(t, "replace.tfstate") + + p := testProvider() + ui := new(cli.MockUi) + c := &StatePushCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{"replace.tfstate"} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + actual := testStateRead(t, "local-state.tfstate") + if !actual.Equal(expected) { + t.Fatalf("bad: %#v", actual) + } +} diff --git a/command/state_show.go b/command/state_show.go index d0f7517af..6be125d7f 100644 --- a/command/state_show.go +++ b/command/state_show.go @@ -26,10 +26,18 @@ func (c *StateShowCommand) Run(args []string) int { } args = cmdFlags.Args() - state, err := c.Meta.State() + // Load the backend + b, err := c.Backend(nil) if err != nil { - c.Ui.Error(fmt.Sprintf(errStateLoadingState, err)) - return cli.RunResultHelp + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + + // Get the state + state, err := b.State() + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) + return 1 } stateReal := state.State() diff --git a/command/taint.go b/command/taint.go index 399f0e330..1d60729a6 100644 --- a/command/taint.go +++ b/command/taint.go @@ -56,8 +56,15 @@ func (c *TaintCommand) Run(args []string) int { return 1 } - // Get the state that we'll be modifying - state, err := c.State() + // Load the backend + b, err := c.Backend(nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + + // Get the state + state, err := b.State() if err != nil { c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) return 1 @@ -122,7 +129,11 @@ func (c *TaintCommand) Run(args []string) int { rs.Taint() log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath()) - if err := c.Meta.PersistState(s); err != nil { + if err := state.WriteState(s); err != nil { + c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) + return 1 + } + if err := state.PersistState(); err != nil { c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) return 1 } diff --git a/command/test-fixtures/backend-change/.terraform/terraform.tfstate b/command/test-fixtures/backend-change/.terraform/terraform.tfstate new file mode 100644 index 000000000..073bd7a82 --- /dev/null +++ b/command/test-fixtures/backend-change/.terraform/terraform.tfstate @@ -0,0 +1,22 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "backend": { + "type": "local", + "config": { + "path": "local-state.tfstate" + }, + "hash": 9073424445967744180 + }, + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": {}, + "depends_on": [] + } + ] +} diff --git a/command/test-fixtures/backend-change/local-state.tfstate b/command/test-fixtures/backend-change/local-state.tfstate new file mode 100644 index 000000000..88c1d86ec --- /dev/null +++ b/command/test-fixtures/backend-change/local-state.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 7, + "lineage": "backend-change" +} diff --git a/command/test-fixtures/backend-change/main.tf b/command/test-fixtures/backend-change/main.tf new file mode 100644 index 000000000..0277003a5 --- /dev/null +++ b/command/test-fixtures/backend-change/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local" { + path = "local-state-2.tfstate" + } +} diff --git a/command/test-fixtures/backend-changed-with-legacy/.terraform/terraform.tfstate b/command/test-fixtures/backend-changed-with-legacy/.terraform/terraform.tfstate new file mode 100644 index 000000000..1e8c0a17d --- /dev/null +++ b/command/test-fixtures/backend-changed-with-legacy/.terraform/terraform.tfstate @@ -0,0 +1,28 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "backend": { + "type": "local", + "config": { + "path": "local-state.tfstate" + }, + "hash": 9073424445967744180 + }, + "remote": { + "type": "local", + "config": { + "path": "local-state-old.tfstate" + } + }, + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": {}, + "depends_on": [] + } + ] +} diff --git a/command/test-fixtures/backend-changed-with-legacy/local-state-old.tfstate b/command/test-fixtures/backend-changed-with-legacy/local-state-old.tfstate new file mode 100644 index 000000000..e9f980b59 --- /dev/null +++ b/command/test-fixtures/backend-changed-with-legacy/local-state-old.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 7, + "lineage": "legacy" +} diff --git a/command/test-fixtures/backend-changed-with-legacy/local-state.tfstate b/command/test-fixtures/backend-changed-with-legacy/local-state.tfstate new file mode 100644 index 000000000..5e9330595 --- /dev/null +++ b/command/test-fixtures/backend-changed-with-legacy/local-state.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 7, + "lineage": "configured" +} diff --git a/command/test-fixtures/backend-changed-with-legacy/main.tf b/command/test-fixtures/backend-changed-with-legacy/main.tf new file mode 100644 index 000000000..0277003a5 --- /dev/null +++ b/command/test-fixtures/backend-changed-with-legacy/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local" { + path = "local-state-2.tfstate" + } +} diff --git a/command/test-fixtures/backend-new-legacy/.terraform/terraform.tfstate b/command/test-fixtures/backend-new-legacy/.terraform/terraform.tfstate new file mode 100644 index 000000000..481edc635 --- /dev/null +++ b/command/test-fixtures/backend-new-legacy/.terraform/terraform.tfstate @@ -0,0 +1,21 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "remote": { + "type": "local", + "config": { + "path": "local-state-old.tfstate" + } + }, + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": {}, + "depends_on": [] + } + ] +} diff --git a/command/test-fixtures/backend-new-legacy/local-state-old.tfstate b/command/test-fixtures/backend-new-legacy/local-state-old.tfstate new file mode 100644 index 000000000..0af594cc4 --- /dev/null +++ b/command/test-fixtures/backend-new-legacy/local-state-old.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 7, + "lineage": "backend-new-legacy" +} diff --git a/command/test-fixtures/backend-new-legacy/main.tf b/command/test-fixtures/backend-new-legacy/main.tf new file mode 100644 index 000000000..ca1bd3921 --- /dev/null +++ b/command/test-fixtures/backend-new-legacy/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local" { + path = "local-state.tfstate" + } +} diff --git a/command/test-fixtures/backend-new-migrate-existing/local-state.tfstate b/command/test-fixtures/backend-new-migrate-existing/local-state.tfstate new file mode 100644 index 000000000..81f6ffebb --- /dev/null +++ b/command/test-fixtures/backend-new-migrate-existing/local-state.tfstate @@ -0,0 +1,16 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 8, + "lineage": "remote", + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": {}, + "depends_on": [] + } + ] +} diff --git a/command/test-fixtures/backend-new-migrate-existing/main.tf b/command/test-fixtures/backend-new-migrate-existing/main.tf new file mode 100644 index 000000000..ca1bd3921 --- /dev/null +++ b/command/test-fixtures/backend-new-migrate-existing/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local" { + path = "local-state.tfstate" + } +} diff --git a/command/test-fixtures/backend-new-migrate-existing/terraform.tfstate b/command/test-fixtures/backend-new-migrate-existing/terraform.tfstate new file mode 100644 index 000000000..7fc619980 --- /dev/null +++ b/command/test-fixtures/backend-new-migrate-existing/terraform.tfstate @@ -0,0 +1,16 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 8, + "lineage": "local", + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": {}, + "depends_on": [] + } + ] +} diff --git a/command/test-fixtures/backend-new-migrate/main.tf b/command/test-fixtures/backend-new-migrate/main.tf new file mode 100644 index 000000000..ca1bd3921 --- /dev/null +++ b/command/test-fixtures/backend-new-migrate/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local" { + path = "local-state.tfstate" + } +} diff --git a/command/test-fixtures/backend-new-migrate/terraform.tfstate b/command/test-fixtures/backend-new-migrate/terraform.tfstate new file mode 100644 index 000000000..b1b1415d0 --- /dev/null +++ b/command/test-fixtures/backend-new-migrate/terraform.tfstate @@ -0,0 +1,16 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 8, + "lineage": "backend-new-migrate", + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": {}, + "depends_on": [] + } + ] +} diff --git a/command/test-fixtures/backend-new/main.tf b/command/test-fixtures/backend-new/main.tf new file mode 100644 index 000000000..ca1bd3921 --- /dev/null +++ b/command/test-fixtures/backend-new/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local" { + path = "local-state.tfstate" + } +} diff --git a/command/test-fixtures/backend-plan-backend-empty-config/.terraform/terraform.tfstate b/command/test-fixtures/backend-plan-backend-empty-config/.terraform/terraform.tfstate new file mode 100644 index 000000000..073bd7a82 --- /dev/null +++ b/command/test-fixtures/backend-plan-backend-empty-config/.terraform/terraform.tfstate @@ -0,0 +1,22 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "backend": { + "type": "local", + "config": { + "path": "local-state.tfstate" + }, + "hash": 9073424445967744180 + }, + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": {}, + "depends_on": [] + } + ] +} diff --git a/command/test-fixtures/backend-plan-backend-empty-config/local-state.tfstate b/command/test-fixtures/backend-plan-backend-empty-config/local-state.tfstate new file mode 100644 index 000000000..48be87380 --- /dev/null +++ b/command/test-fixtures/backend-plan-backend-empty-config/local-state.tfstate @@ -0,0 +1,5 @@ +{ + "version": 3, + "serial": 0, + "lineage": "hello" +} diff --git a/command/test-fixtures/backend-plan-backend-empty-config/main.tf b/command/test-fixtures/backend-plan-backend-empty-config/main.tf new file mode 100644 index 000000000..ca1bd3921 --- /dev/null +++ b/command/test-fixtures/backend-plan-backend-empty-config/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local" { + path = "local-state.tfstate" + } +} diff --git a/command/test-fixtures/backend-plan-backend-empty/readme.txt b/command/test-fixtures/backend-plan-backend-empty/readme.txt new file mode 100644 index 000000000..e2d6fa209 --- /dev/null +++ b/command/test-fixtures/backend-plan-backend-empty/readme.txt @@ -0,0 +1 @@ +This directory is empty on purpose. diff --git a/command/test-fixtures/backend-plan-backend-match/local-state.tfstate b/command/test-fixtures/backend-plan-backend-match/local-state.tfstate new file mode 100644 index 000000000..48be87380 --- /dev/null +++ b/command/test-fixtures/backend-plan-backend-match/local-state.tfstate @@ -0,0 +1,5 @@ +{ + "version": 3, + "serial": 0, + "lineage": "hello" +} diff --git a/command/test-fixtures/backend-plan-backend-match/readme.txt b/command/test-fixtures/backend-plan-backend-match/readme.txt new file mode 100644 index 000000000..b3817536d --- /dev/null +++ b/command/test-fixtures/backend-plan-backend-match/readme.txt @@ -0,0 +1 @@ +This directory has no configuration on purpose. diff --git a/command/test-fixtures/backend-plan-backend-mismatch/local-state.tfstate b/command/test-fixtures/backend-plan-backend-mismatch/local-state.tfstate new file mode 100644 index 000000000..50101996a --- /dev/null +++ b/command/test-fixtures/backend-plan-backend-mismatch/local-state.tfstate @@ -0,0 +1,5 @@ +{ + "version": 3, + "serial": 0, + "lineage": "different" +} diff --git a/command/test-fixtures/backend-plan-legacy-data/local-state.tfstate b/command/test-fixtures/backend-plan-legacy-data/local-state.tfstate new file mode 100644 index 000000000..48be87380 --- /dev/null +++ b/command/test-fixtures/backend-plan-legacy-data/local-state.tfstate @@ -0,0 +1,5 @@ +{ + "version": 3, + "serial": 0, + "lineage": "hello" +} diff --git a/command/test-fixtures/backend-plan-legacy-data/main.tf b/command/test-fixtures/backend-plan-legacy-data/main.tf new file mode 100644 index 000000000..b7db25411 --- /dev/null +++ b/command/test-fixtures/backend-plan-legacy-data/main.tf @@ -0,0 +1 @@ +# Empty diff --git a/command/test-fixtures/backend-plan-legacy-data/state.tfstate b/command/test-fixtures/backend-plan-legacy-data/state.tfstate new file mode 100644 index 000000000..195441f37 --- /dev/null +++ b/command/test-fixtures/backend-plan-legacy-data/state.tfstate @@ -0,0 +1,21 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "remote": { + "type": "local", + "config": { + "path": "local-state.tfstate" + } + }, + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": {}, + "depends_on": [] + } + ] +} diff --git a/command/test-fixtures/backend-plan-legacy/readme.txt b/command/test-fixtures/backend-plan-legacy/readme.txt new file mode 100644 index 000000000..08c2a3505 --- /dev/null +++ b/command/test-fixtures/backend-plan-legacy/readme.txt @@ -0,0 +1 @@ +No configs on purpose diff --git a/command/test-fixtures/backend-plan-local-match/main.tf b/command/test-fixtures/backend-plan-local-match/main.tf new file mode 100644 index 000000000..b7db25411 --- /dev/null +++ b/command/test-fixtures/backend-plan-local-match/main.tf @@ -0,0 +1 @@ +# Empty diff --git a/command/test-fixtures/backend-plan-local-match/terraform.tfstate b/command/test-fixtures/backend-plan-local-match/terraform.tfstate new file mode 100644 index 000000000..9070c5841 --- /dev/null +++ b/command/test-fixtures/backend-plan-local-match/terraform.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 7, + "lineage": "hello" +} diff --git a/command/test-fixtures/backend-plan-local-mismatch-lineage/main.tf b/command/test-fixtures/backend-plan-local-mismatch-lineage/main.tf new file mode 100644 index 000000000..b7db25411 --- /dev/null +++ b/command/test-fixtures/backend-plan-local-mismatch-lineage/main.tf @@ -0,0 +1 @@ +# Empty diff --git a/command/test-fixtures/backend-plan-local-mismatch-lineage/terraform.tfstate b/command/test-fixtures/backend-plan-local-mismatch-lineage/terraform.tfstate new file mode 100644 index 000000000..9070c5841 --- /dev/null +++ b/command/test-fixtures/backend-plan-local-mismatch-lineage/terraform.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 7, + "lineage": "hello" +} diff --git a/command/test-fixtures/backend-plan-local-newer/main.tf b/command/test-fixtures/backend-plan-local-newer/main.tf new file mode 100644 index 000000000..b7db25411 --- /dev/null +++ b/command/test-fixtures/backend-plan-local-newer/main.tf @@ -0,0 +1 @@ +# Empty diff --git a/command/test-fixtures/backend-plan-local-newer/terraform.tfstate b/command/test-fixtures/backend-plan-local-newer/terraform.tfstate new file mode 100644 index 000000000..776fab46f --- /dev/null +++ b/command/test-fixtures/backend-plan-local-newer/terraform.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 10, + "lineage": "hello" +} diff --git a/command/test-fixtures/backend-plan-local/main.tf b/command/test-fixtures/backend-plan-local/main.tf new file mode 100644 index 000000000..fec56017d --- /dev/null +++ b/command/test-fixtures/backend-plan-local/main.tf @@ -0,0 +1 @@ +# Hello diff --git a/command/test-fixtures/backend-unchanged-with-legacy/.terraform/terraform.tfstate b/command/test-fixtures/backend-unchanged-with-legacy/.terraform/terraform.tfstate new file mode 100644 index 000000000..1e8c0a17d --- /dev/null +++ b/command/test-fixtures/backend-unchanged-with-legacy/.terraform/terraform.tfstate @@ -0,0 +1,28 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "backend": { + "type": "local", + "config": { + "path": "local-state.tfstate" + }, + "hash": 9073424445967744180 + }, + "remote": { + "type": "local", + "config": { + "path": "local-state-old.tfstate" + } + }, + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": {}, + "depends_on": [] + } + ] +} diff --git a/command/test-fixtures/backend-unchanged-with-legacy/local-state-old.tfstate b/command/test-fixtures/backend-unchanged-with-legacy/local-state-old.tfstate new file mode 100644 index 000000000..7ebed86ad --- /dev/null +++ b/command/test-fixtures/backend-unchanged-with-legacy/local-state-old.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 7, + "lineage": "backend-unchanged-with-legacy" +} diff --git a/command/test-fixtures/backend-unchanged-with-legacy/local-state.tfstate b/command/test-fixtures/backend-unchanged-with-legacy/local-state.tfstate new file mode 100644 index 000000000..5e9330595 --- /dev/null +++ b/command/test-fixtures/backend-unchanged-with-legacy/local-state.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 7, + "lineage": "configured" +} diff --git a/command/test-fixtures/backend-unchanged-with-legacy/main.tf b/command/test-fixtures/backend-unchanged-with-legacy/main.tf new file mode 100644 index 000000000..ca1bd3921 --- /dev/null +++ b/command/test-fixtures/backend-unchanged-with-legacy/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local" { + path = "local-state.tfstate" + } +} diff --git a/command/test-fixtures/backend-unchanged/.terraform/terraform.tfstate b/command/test-fixtures/backend-unchanged/.terraform/terraform.tfstate new file mode 100644 index 000000000..073bd7a82 --- /dev/null +++ b/command/test-fixtures/backend-unchanged/.terraform/terraform.tfstate @@ -0,0 +1,22 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "backend": { + "type": "local", + "config": { + "path": "local-state.tfstate" + }, + "hash": 9073424445967744180 + }, + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": {}, + "depends_on": [] + } + ] +} diff --git a/command/test-fixtures/backend-unchanged/local-state.tfstate b/command/test-fixtures/backend-unchanged/local-state.tfstate new file mode 100644 index 000000000..5ed2459a9 --- /dev/null +++ b/command/test-fixtures/backend-unchanged/local-state.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 7, + "lineage": "configuredUnchanged" +} diff --git a/command/test-fixtures/backend-unchanged/main.tf b/command/test-fixtures/backend-unchanged/main.tf new file mode 100644 index 000000000..ca1bd3921 --- /dev/null +++ b/command/test-fixtures/backend-unchanged/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local" { + path = "local-state.tfstate" + } +} diff --git a/command/test-fixtures/backend-unset-with-legacy/.terraform/terraform.tfstate b/command/test-fixtures/backend-unset-with-legacy/.terraform/terraform.tfstate new file mode 100644 index 000000000..1e8c0a17d --- /dev/null +++ b/command/test-fixtures/backend-unset-with-legacy/.terraform/terraform.tfstate @@ -0,0 +1,28 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "backend": { + "type": "local", + "config": { + "path": "local-state.tfstate" + }, + "hash": 9073424445967744180 + }, + "remote": { + "type": "local", + "config": { + "path": "local-state-old.tfstate" + } + }, + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": {}, + "depends_on": [] + } + ] +} diff --git a/command/test-fixtures/backend-unset-with-legacy/local-state-old.tfstate b/command/test-fixtures/backend-unset-with-legacy/local-state-old.tfstate new file mode 100644 index 000000000..e9f980b59 --- /dev/null +++ b/command/test-fixtures/backend-unset-with-legacy/local-state-old.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 7, + "lineage": "legacy" +} diff --git a/command/test-fixtures/backend-unset-with-legacy/local-state.tfstate b/command/test-fixtures/backend-unset-with-legacy/local-state.tfstate new file mode 100644 index 000000000..18a4c8009 --- /dev/null +++ b/command/test-fixtures/backend-unset-with-legacy/local-state.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 7, + "lineage": "backend" +} diff --git a/command/test-fixtures/backend-unset-with-legacy/main.tf b/command/test-fixtures/backend-unset-with-legacy/main.tf new file mode 100644 index 000000000..0422cd4b6 --- /dev/null +++ b/command/test-fixtures/backend-unset-with-legacy/main.tf @@ -0,0 +1 @@ +# Empty, we're unsetting diff --git a/command/test-fixtures/backend-unset/.terraform/terraform.tfstate b/command/test-fixtures/backend-unset/.terraform/terraform.tfstate new file mode 100644 index 000000000..073bd7a82 --- /dev/null +++ b/command/test-fixtures/backend-unset/.terraform/terraform.tfstate @@ -0,0 +1,22 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "backend": { + "type": "local", + "config": { + "path": "local-state.tfstate" + }, + "hash": 9073424445967744180 + }, + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": {}, + "depends_on": [] + } + ] +} diff --git a/command/test-fixtures/backend-unset/local-state.tfstate b/command/test-fixtures/backend-unset/local-state.tfstate new file mode 100644 index 000000000..51d588030 --- /dev/null +++ b/command/test-fixtures/backend-unset/local-state.tfstate @@ -0,0 +1,6 @@ +{ + "version": 3, + "terraform_version": "0.8.2", + "serial": 7, + "lineage": "configuredUnset" +} diff --git a/command/test-fixtures/backend-unset/main.tf b/command/test-fixtures/backend-unset/main.tf new file mode 100644 index 000000000..3571c40e3 --- /dev/null +++ b/command/test-fixtures/backend-unset/main.tf @@ -0,0 +1 @@ +# Empty, unset! diff --git a/command/test-fixtures/init-backend-config-file/input.config b/command/test-fixtures/init-backend-config-file/input.config new file mode 100644 index 000000000..6cd14f4a3 --- /dev/null +++ b/command/test-fixtures/init-backend-config-file/input.config @@ -0,0 +1 @@ +path = "hello" diff --git a/command/test-fixtures/init-backend-config-file/main.tf b/command/test-fixtures/init-backend-config-file/main.tf new file mode 100644 index 000000000..c08b42fb0 --- /dev/null +++ b/command/test-fixtures/init-backend-config-file/main.tf @@ -0,0 +1,3 @@ +terraform { + backend "local" {} +} diff --git a/command/test-fixtures/init-backend/main.tf b/command/test-fixtures/init-backend/main.tf new file mode 100644 index 000000000..a6bafdab8 --- /dev/null +++ b/command/test-fixtures/init-backend/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local" { + path = "foo" + } +} diff --git a/command/test-fixtures/init-get/foo/main.tf b/command/test-fixtures/init-get/foo/main.tf new file mode 100644 index 000000000..b7db25411 --- /dev/null +++ b/command/test-fixtures/init-get/foo/main.tf @@ -0,0 +1 @@ +# Empty diff --git a/command/test-fixtures/init-get/main.tf b/command/test-fixtures/init-get/main.tf new file mode 100644 index 000000000..0ce1c38d3 --- /dev/null +++ b/command/test-fixtures/init-get/main.tf @@ -0,0 +1,3 @@ +module "foo" { + source = "./foo" +} diff --git a/command/test-fixtures/plan-out-backend-legacy/main.tf b/command/test-fixtures/plan-out-backend-legacy/main.tf new file mode 100644 index 000000000..1b1012991 --- /dev/null +++ b/command/test-fixtures/plan-out-backend-legacy/main.tf @@ -0,0 +1,3 @@ +resource "test_instance" "foo" { + ami = "bar" +} diff --git a/command/test-fixtures/plan-out-backend/main.tf b/command/test-fixtures/plan-out-backend/main.tf new file mode 100644 index 000000000..e1be95fa8 --- /dev/null +++ b/command/test-fixtures/plan-out-backend/main.tf @@ -0,0 +1,9 @@ +terraform { + backend "http" { + test = true + } +} + +resource "test_instance" "foo" { + ami = "bar" +} diff --git a/command/test-fixtures/state-push-bad-lineage/.terraform/terraform.tfstate b/command/test-fixtures/state-push-bad-lineage/.terraform/terraform.tfstate new file mode 100644 index 000000000..073bd7a82 --- /dev/null +++ b/command/test-fixtures/state-push-bad-lineage/.terraform/terraform.tfstate @@ -0,0 +1,22 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "backend": { + "type": "local", + "config": { + "path": "local-state.tfstate" + }, + "hash": 9073424445967744180 + }, + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": {}, + "depends_on": [] + } + ] +} diff --git a/command/test-fixtures/state-push-bad-lineage/local-state.tfstate b/command/test-fixtures/state-push-bad-lineage/local-state.tfstate new file mode 100644 index 000000000..4023b53e0 --- /dev/null +++ b/command/test-fixtures/state-push-bad-lineage/local-state.tfstate @@ -0,0 +1,5 @@ +{ + "version": 3, + "serial": 1, + "lineage": "mismatch" +} diff --git a/command/test-fixtures/state-push-bad-lineage/main.tf b/command/test-fixtures/state-push-bad-lineage/main.tf new file mode 100644 index 000000000..ca1bd3921 --- /dev/null +++ b/command/test-fixtures/state-push-bad-lineage/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local" { + path = "local-state.tfstate" + } +} diff --git a/command/test-fixtures/state-push-bad-lineage/replace.tfstate b/command/test-fixtures/state-push-bad-lineage/replace.tfstate new file mode 100644 index 000000000..0e3b7013a --- /dev/null +++ b/command/test-fixtures/state-push-bad-lineage/replace.tfstate @@ -0,0 +1,5 @@ +{ + "version": 3, + "serial": 2, + "lineage": "hello" +} diff --git a/command/test-fixtures/state-push-good/.terraform/terraform.tfstate b/command/test-fixtures/state-push-good/.terraform/terraform.tfstate new file mode 100644 index 000000000..073bd7a82 --- /dev/null +++ b/command/test-fixtures/state-push-good/.terraform/terraform.tfstate @@ -0,0 +1,22 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "backend": { + "type": "local", + "config": { + "path": "local-state.tfstate" + }, + "hash": 9073424445967744180 + }, + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": {}, + "depends_on": [] + } + ] +} diff --git a/command/test-fixtures/state-push-good/main.tf b/command/test-fixtures/state-push-good/main.tf new file mode 100644 index 000000000..ca1bd3921 --- /dev/null +++ b/command/test-fixtures/state-push-good/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local" { + path = "local-state.tfstate" + } +} diff --git a/command/test-fixtures/state-push-good/replace.tfstate b/command/test-fixtures/state-push-good/replace.tfstate new file mode 100644 index 000000000..48be87380 --- /dev/null +++ b/command/test-fixtures/state-push-good/replace.tfstate @@ -0,0 +1,5 @@ +{ + "version": 3, + "serial": 0, + "lineage": "hello" +} diff --git a/command/test-fixtures/state-push-replace-match/.terraform/terraform.tfstate b/command/test-fixtures/state-push-replace-match/.terraform/terraform.tfstate new file mode 100644 index 000000000..073bd7a82 --- /dev/null +++ b/command/test-fixtures/state-push-replace-match/.terraform/terraform.tfstate @@ -0,0 +1,22 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "backend": { + "type": "local", + "config": { + "path": "local-state.tfstate" + }, + "hash": 9073424445967744180 + }, + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": {}, + "depends_on": [] + } + ] +} diff --git a/command/test-fixtures/state-push-replace-match/local-state.tfstate b/command/test-fixtures/state-push-replace-match/local-state.tfstate new file mode 100644 index 000000000..8dd356bc9 --- /dev/null +++ b/command/test-fixtures/state-push-replace-match/local-state.tfstate @@ -0,0 +1,5 @@ +{ + "version": 3, + "serial": 1, + "lineage": "hello" +} diff --git a/command/test-fixtures/state-push-replace-match/main.tf b/command/test-fixtures/state-push-replace-match/main.tf new file mode 100644 index 000000000..ca1bd3921 --- /dev/null +++ b/command/test-fixtures/state-push-replace-match/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local" { + path = "local-state.tfstate" + } +} diff --git a/command/test-fixtures/state-push-replace-match/replace.tfstate b/command/test-fixtures/state-push-replace-match/replace.tfstate new file mode 100644 index 000000000..0e3b7013a --- /dev/null +++ b/command/test-fixtures/state-push-replace-match/replace.tfstate @@ -0,0 +1,5 @@ +{ + "version": 3, + "serial": 2, + "lineage": "hello" +} diff --git a/command/test-fixtures/state-push-serial-newer/.terraform/terraform.tfstate b/command/test-fixtures/state-push-serial-newer/.terraform/terraform.tfstate new file mode 100644 index 000000000..073bd7a82 --- /dev/null +++ b/command/test-fixtures/state-push-serial-newer/.terraform/terraform.tfstate @@ -0,0 +1,22 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "backend": { + "type": "local", + "config": { + "path": "local-state.tfstate" + }, + "hash": 9073424445967744180 + }, + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": {}, + "depends_on": [] + } + ] +} diff --git a/command/test-fixtures/state-push-serial-newer/local-state.tfstate b/command/test-fixtures/state-push-serial-newer/local-state.tfstate new file mode 100644 index 000000000..c114b190d --- /dev/null +++ b/command/test-fixtures/state-push-serial-newer/local-state.tfstate @@ -0,0 +1,5 @@ +{ + "version": 3, + "serial": 3, + "lineage": "hello" +} diff --git a/command/test-fixtures/state-push-serial-newer/main.tf b/command/test-fixtures/state-push-serial-newer/main.tf new file mode 100644 index 000000000..ca1bd3921 --- /dev/null +++ b/command/test-fixtures/state-push-serial-newer/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local" { + path = "local-state.tfstate" + } +} diff --git a/command/test-fixtures/state-push-serial-newer/replace.tfstate b/command/test-fixtures/state-push-serial-newer/replace.tfstate new file mode 100644 index 000000000..0e3b7013a --- /dev/null +++ b/command/test-fixtures/state-push-serial-newer/replace.tfstate @@ -0,0 +1,5 @@ +{ + "version": 3, + "serial": 2, + "lineage": "hello" +} diff --git a/command/test-fixtures/state-push-serial-older/.terraform/terraform.tfstate b/command/test-fixtures/state-push-serial-older/.terraform/terraform.tfstate new file mode 100644 index 000000000..073bd7a82 --- /dev/null +++ b/command/test-fixtures/state-push-serial-older/.terraform/terraform.tfstate @@ -0,0 +1,22 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "backend": { + "type": "local", + "config": { + "path": "local-state.tfstate" + }, + "hash": 9073424445967744180 + }, + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": {}, + "depends_on": [] + } + ] +} diff --git a/command/test-fixtures/state-push-serial-older/local-state.tfstate b/command/test-fixtures/state-push-serial-older/local-state.tfstate new file mode 100644 index 000000000..8dd356bc9 --- /dev/null +++ b/command/test-fixtures/state-push-serial-older/local-state.tfstate @@ -0,0 +1,5 @@ +{ + "version": 3, + "serial": 1, + "lineage": "hello" +} diff --git a/command/test-fixtures/state-push-serial-older/main.tf b/command/test-fixtures/state-push-serial-older/main.tf new file mode 100644 index 000000000..ca1bd3921 --- /dev/null +++ b/command/test-fixtures/state-push-serial-older/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "local" { + path = "local-state.tfstate" + } +} diff --git a/command/test-fixtures/state-push-serial-older/replace.tfstate b/command/test-fixtures/state-push-serial-older/replace.tfstate new file mode 100644 index 000000000..0e3b7013a --- /dev/null +++ b/command/test-fixtures/state-push-serial-older/replace.tfstate @@ -0,0 +1,5 @@ +{ + "version": 3, + "serial": 2, + "lineage": "hello" +} diff --git a/command/ui_input.go b/command/ui_input.go index bc666109d..fad684d72 100644 --- a/command/ui_input.go +++ b/command/ui_input.go @@ -19,6 +19,7 @@ import ( var defaultInputReader io.Reader var defaultInputWriter io.Writer +var testInputResponse []string // UIInput is an implementation of terraform.UIInput that asks the CLI // for input stdin. @@ -64,6 +65,15 @@ func (i *UIInput) Input(opts *terraform.InputOpts) (string, error) { return "", errors.New("interrupted") } + // If we have test results, return those + if testInputResponse != nil { + v := testInputResponse[0] + testInputResponse = testInputResponse[1:] + return v, nil + } + + log.Printf("[DEBUG] command: asking for input: %q", opts.Query) + // Listen for interrupts so we can cancel the input ask sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt) diff --git a/command/untaint.go b/command/untaint.go index 099c94859..4be0590fa 100644 --- a/command/untaint.go +++ b/command/untaint.go @@ -43,8 +43,15 @@ func (c *UntaintCommand) Run(args []string) int { module = "root." + module } - // Get the state that we'll be modifying - state, err := c.State() + // Load the backend + b, err := c.Backend(nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + + // Get the state + state, err := b.State() if err != nil { c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) return 1 @@ -109,7 +116,11 @@ func (c *UntaintCommand) Run(args []string) int { rs.Untaint() log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath()) - if err := c.Meta.PersistState(s); err != nil { + if err := state.WriteState(s); err != nil { + c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) + return 1 + } + if err := state.PersistState(); err != nil { c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) return 1 }