diff --git a/command/refresh.go b/command/refresh.go index a1229c9b7..420f84678 100644 --- a/command/refresh.go +++ b/command/refresh.go @@ -7,7 +7,6 @@ import ( "os" "strings" - "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) @@ -20,68 +19,57 @@ type RefreshCommand struct { } func (c *RefreshCommand) Run(args []string) int { - var outPath string - statePath := "terraform.tfstate" - configPath := "." + var statePath, stateOutPath string cmdFlags := flag.NewFlagSet("refresh", flag.ContinueOnError) - cmdFlags.StringVar(&outPath, "out", "", "output path") + cmdFlags.StringVar(&statePath, "state", DefaultStateFilename, "path") + cmdFlags.StringVar(&stateOutPath, "state-out", "", "path") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { return 1 } + var configPath string args = cmdFlags.Args() - if len(args) != 2 { - // TODO(mitchellh): this is temporary until we can assume current - // dir for Terraform config. - c.Ui.Error("TEMPORARY: The refresh command requires two args.") + if len(args) > 1 { + c.Ui.Error("The apply command expacts 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)) + } } - statePath = args[0] - configPath = args[1] - if outPath == "" { - outPath = statePath + // If we don't specify an output path, default to out normal state + // path. + if stateOutPath == "" { + stateOutPath = statePath } - // Load up the state - f, err := os.Open(statePath) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error loading state: %s", err)) - return 1 - } - - state, err := terraform.ReadState(f) - f.Close() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error loading state: %s", err)) - return 1 - } - - b, err := config.Load(configPath) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error loading blueprint: %s", err)) - return 1 - } - - c.ContextOpts.Config = b - c.ContextOpts.State = state + // Build the context based on the arguments given c.ContextOpts.Hooks = append(c.ContextOpts.Hooks, &UiHook{Ui: c.Ui}) - ctx := terraform.NewContext(c.ContextOpts) + ctx, err := ContextArg(configPath, statePath, c.ContextOpts) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } if !validateContext(ctx, c.Ui) { return 1 } - state, err = ctx.Refresh() + state, err := ctx.Refresh() if err != nil { c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err)) return 1 } - log.Printf("[INFO] Writing state output to: %s", outPath) - f, err = os.Create(outPath) + log.Printf("[INFO] Writing state output to: %s", stateOutPath) + f, err := os.Create(stateOutPath) if err == nil { defer f.Close() err = terraform.WriteState(state, f) @@ -96,21 +84,27 @@ func (c *RefreshCommand) Run(args []string) int { func (c *RefreshCommand) Help() string { helpText := ` -Usage: terraform refresh [options] [terraform.tfstate] [terraform.tf] +Usage: terraform refresh [options] [dir] - Refresh and update the state of your infrastructure. This is read-only - operation that will not modify infrastructure. The read-only property - is dependent on resource providers being implemented correctly. + Update the state file of your infrastructure with metadata that matches + the physical resources they are tracking. + + This will not modify your infrastructure, but it can modify your + state file to update metadata. This metadata might cause new changes + to occur when you generate a plan or call apply next. Options: - -out=path Path to write updated state file. If this is not specified, - the existing state file will be overridden. + -state=path Path to read and save state (unless state-out + is specified). Defaults to "terraform.tfstate". + + -state-out=path Path to write updated state file. By default, the + "-state" path will be used. ` return strings.TrimSpace(helpText) } func (c *RefreshCommand) Synopsis() string { - return "Refresh the state of your infrastructure" + return "Refresh the local state of your infrastructure" } diff --git a/command/refresh_test.go b/command/refresh_test.go index f6332c5ad..643b2daf6 100644 --- a/command/refresh_test.go +++ b/command/refresh_test.go @@ -3,6 +3,7 @@ package command import ( "io/ioutil" "os" + "path/filepath" "reflect" "testing" @@ -32,7 +33,7 @@ func TestRefresh(t *testing.T) { p.RefreshReturn = &terraform.ResourceState{ID: "yes"} args := []string{ - statePath, + "-state", statePath, testFixturePath("refresh"), } if code := c.Run(args); code != 0 { @@ -61,6 +62,142 @@ func TestRefresh(t *testing.T) { } } +func TestRefresh_cwd(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.Chdir(testFixturePath("refresh")); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Chdir(cwd) + + state := &terraform.State{ + Resources: map[string]*terraform.ResourceState{ + "test_instance.foo": &terraform.ResourceState{ + ID: "bar", + Type: "test_instance", + }, + }, + } + statePath := testStateFile(t, state) + + p := testProvider() + ui := new(cli.MockUi) + c := &RefreshCommand{ + ContextOpts: testCtxConfig(p), + Ui: ui, + } + + p.RefreshFn = nil + p.RefreshReturn = &terraform.ResourceState{ID: "yes"} + + args := []string{ + "-state", statePath, + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + if !p.RefreshCalled { + t.Fatal("refresh should be called") + } + + f, err := os.Open(statePath) + if err != nil { + t.Fatalf("err: %s", err) + } + + newState, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := newState.Resources["test_instance.foo"] + expected := p.RefreshReturn + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } +} + +func TestRefresh_defaultState(t *testing.T) { + originalState := &terraform.State{ + Resources: map[string]*terraform.ResourceState{ + "test_instance.foo": &terraform.ResourceState{ + ID: "bar", + Type: "test_instance", + }, + }, + } + + // Write the state file in a temporary directory with the + // default filename. + td, err := ioutil.TempDir("", "tf") + if err != nil { + t.Fatalf("err: %s", err) + } + statePath := filepath.Join(td, DefaultStateFilename) + + f, err := os.Create(statePath) + if err != nil { + t.Fatalf("err: %s", err) + } + err = terraform.WriteState(originalState, f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + // Change to that directory + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.Chdir(filepath.Dir(statePath)); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Chdir(cwd) + + p := testProvider() + ui := new(cli.MockUi) + c := &RefreshCommand{ + ContextOpts: testCtxConfig(p), + Ui: ui, + } + + p.RefreshFn = nil + p.RefreshReturn = &terraform.ResourceState{ID: "yes"} + + args := []string{ + testFixturePath("refresh"), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + if !p.RefreshCalled { + t.Fatal("refresh should be called") + } + + f, err = os.Open(statePath) + if err != nil { + t.Fatalf("err: %s", err) + } + + newState, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := newState.Resources["test_instance.foo"] + expected := p.RefreshReturn + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } +} + func TestRefresh_outPath(t *testing.T) { state := &terraform.State{ Resources: map[string]*terraform.ResourceState{ @@ -92,8 +229,8 @@ func TestRefresh_outPath(t *testing.T) { p.RefreshReturn = &terraform.ResourceState{ID: "yes"} args := []string{ - "-out", outPath, - statePath, + "-state", statePath, + "-state-out", outPath, testFixturePath("refresh"), } if code := c.Run(args); code != 0 {