From ff2d753062b9a9d40dafbdb02839c2aff0701dfc Mon Sep 17 00:00:00 2001 From: James Bardin Date: Wed, 29 Mar 2017 12:50:20 -0400 Subject: [PATCH] add Rehash to terraform.BackendState This method mirrors that of config.Backend, so we can compare the configration of a backend read from a config vs that of a backend read from a state. This will prevent init from reinitializing when using `-backend-config` options that match the existing state. --- command/init_test.go | 58 +++++++++++++++++++ command/meta_backend.go | 18 +++++- .../test-fixtures/init-backend-empty/main.tf | 4 ++ config/config_terraform.go | 2 +- terraform/state.go | 27 +++++++++ 5 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 command/test-fixtures/init-backend-empty/main.tf diff --git a/command/init_test.go b/command/init_test.go index dee54495d..ede29cfc3 100644 --- a/command/init_test.go +++ b/command/init_test.go @@ -406,6 +406,64 @@ func TestInit_copyBackendDst(t *testing.T) { } } +func TestInit_backendReinitWithExtra(t *testing.T) { + td := tempDir(t) + copy.CopyDir(testFixturePath("init-backend-empty"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + m := testMetaBackend(t, nil) + opts := &BackendOpts{ + ConfigExtra: map[string]interface{}{"path": "hello"}, + Init: true, + } + + b, err := m.backendConfig(opts) + if err != nil { + t.Fatal(err) + } + + ui := new(cli.MockUi) + c := &InitCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{"-backend-config", "path=hello"} + 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) + } + + if state.Backend.Hash != b.Hash { + t.Fatal("mismatched state and config backend hashes") + } + + if state.Backend.Rehash() != b.Rehash() { + t.Fatal("mismatched state and config re-hashes") + } + + // init again and make sure nothing changes + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + state = testStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) + if v := state.Backend.Config["path"]; v != "hello" { + t.Fatalf("bad: %#v", v) + } + + if state.Backend.Hash != b.Hash { + t.Fatal("mismatched state and config backend hashes") + } +} + /* func TestInit_remoteState(t *testing.T) { tmp, cwd := testCwd(t) diff --git a/command/meta_backend.go b/command/meta_backend.go index b30659dc0..8cf639044 100644 --- a/command/meta_backend.go +++ b/command/meta_backend.go @@ -415,8 +415,16 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, error) { case c != nil && s.Remote.Empty() && !s.Backend.Empty(): // If our configuration is the same, then we're just initializing // a previously configured remote backend. - if !s.Backend.Empty() && s.Backend.Hash == cHash { - return m.backend_C_r_S_unchanged(c, sMgr) + if !s.Backend.Empty() { + hash := s.Backend.Hash + // on init we need an updated hash containing any extra options + // that were added after merging. + if opts.Init { + hash = s.Backend.Rehash() + } + if hash == cHash { + return m.backend_C_r_S_unchanged(c, sMgr) + } } if !opts.Init { @@ -451,7 +459,11 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, error) { case c != nil && !s.Remote.Empty() && !s.Backend.Empty(): // If the hashes are the same, we have a legacy remote state with // an unchanged stored backend state. - if s.Backend.Hash == cHash { + hash := s.Backend.Hash + if opts.Init { + hash = s.Backend.Rehash() + } + if hash == cHash { if !opts.Init { initReason := fmt.Sprintf( "Legacy remote state found with configured backend %q", diff --git a/command/test-fixtures/init-backend-empty/main.tf b/command/test-fixtures/init-backend-empty/main.tf new file mode 100644 index 000000000..7f62e0e19 --- /dev/null +++ b/command/test-fixtures/init-backend-empty/main.tf @@ -0,0 +1,4 @@ +terraform { + backend "local" { + } +} diff --git a/config/config_terraform.go b/config/config_terraform.go index a547cc798..8535c9648 100644 --- a/config/config_terraform.go +++ b/config/config_terraform.go @@ -72,7 +72,7 @@ type Backend struct { Hash uint64 } -// Hash returns a unique content hash for this backend's configuration +// Rehash returns a unique content hash for this backend's configuration // as a uint64 value. func (b *Backend) Rehash() uint64 { // If we have no backend, the value is zero diff --git a/terraform/state.go b/terraform/state.go index 4e5aa713f..d5adefca1 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -20,6 +20,7 @@ import ( "github.com/hashicorp/go-version" "github.com/hashicorp/terraform/config" "github.com/mitchellh/copystructure" + "github.com/mitchellh/hashstructure" "github.com/satori/go.uuid" ) @@ -801,6 +802,32 @@ func (s *BackendState) Empty() bool { return s == nil || s.Type == "" } +// Rehash returns a unique content hash for this backend's configuration +// as a uint64 value. +// The Hash stored in the backend state needs to match the config itself, but +// we need to compare the backend config after it has been combined with all +// options. +// This function must match the implementation used by config.Backend. +func (s *BackendState) Rehash() uint64 { + if s == nil { + return 0 + } + + // Use hashstructure to hash only our type with the config. + code, err := hashstructure.Hash(map[string]interface{}{ + "type": s.Type, + "config": s.Config, + }, nil) + + // This should never happen since we have just some basic primitives + // so panic if there is an error. + if err != nil { + panic(err) + } + + return code +} + // RemoteState is used to track the information about a remote // state store that we push/pull state to. type RemoteState struct {