From 032f71f1ff8f54c9eb76a1fe517ab7669504a967 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Wed, 24 May 2017 17:35:46 -0700 Subject: [PATCH] command: produce provider lock file during "terraform init" Once we've installed the necessary plugins, we'll do one more walk of the available plugins and record the SHA256 hashes of all of the plugins we select in the provider lock file. The file we write here gets read when we're building ContextOpts to initialize the main terraform context, so any command that works with the context will then fail if any of the provider binaries change. --- command/init.go | 23 ++++++++- command/init_test.go | 48 +++++++++++++++++++ command/plugins.go | 24 +++++++--- command/push_test.go | 5 ++ .../init-provider-lock-file/main.tf | 3 ++ .../test-fixtures/init-providers-lock/main.tf | 3 ++ 6 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 command/test-fixtures/init-provider-lock-file/main.tf create mode 100644 command/test-fixtures/init-providers-lock/main.tf diff --git a/command/init.go b/command/init.go index db6ec0fe25..7afeb88304 100644 --- a/command/init.go +++ b/command/init.go @@ -216,8 +216,9 @@ func (c *InitCommand) getProviders(path string, state *terraform.State) error { return err } + available := c.providerPluginSet() requirements := terraform.ModuleTreeDependencies(mod, state).AllPluginRequirements() - missing := c.missingProviders(requirements) + missing := c.missingPlugins(available, requirements) dst := c.pluginDir() for provider, reqd := range missing { @@ -227,6 +228,26 @@ func (c *InitCommand) getProviders(path string, state *terraform.State) error { return err } } + + // With all the providers downloaded, we'll generate our lock file + // that ensures the provider binaries remain unchanged until we init + // again. If anything changes, other commands that use providers will + // fail with an error instructing the user to re-run this command. + available = c.providerPluginSet() // re-discover to see newly-installed plugins + chosen := choosePlugins(available, requirements) + digests := map[string][]byte{} + for name, meta := range chosen { + digest, err := meta.SHA256() + if err != nil { + return fmt.Errorf("failed to read provider plugin %s: %s", meta.Path, err) + } + digests[name] = digest + } + err = c.providerPluginsLock().Write(digests) + if err != nil { + return fmt.Errorf("failed to save provider manifest: %s", err) + } + return nil } diff --git a/command/init_test.go b/command/init_test.go index a95a5155b3..f41f44f4e9 100644 --- a/command/init_test.go +++ b/command/init_test.go @@ -1,9 +1,11 @@ package command import ( + "fmt" "io/ioutil" "os" "path/filepath" + "runtime" "strings" "testing" @@ -618,6 +620,52 @@ func TestInit_getProviderMissing(t *testing.T) { } } +func TestInit_providerLockFile(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + copy.CopyDir(testFixturePath("init-provider-lock-file"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + getter := &mockGetProvider{ + Providers: map[string][]string{ + "test": []string{"1.2.3"}, + }, + } + + ui := new(cli.MockUi) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + }, + getProvider: getter.GetProvider, + } + + args := []string{} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + providersLockFile := fmt.Sprintf( + ".terraform/plugins/%s_%s/providers.json", + runtime.GOOS, runtime.GOARCH, + ) + buf, err := ioutil.ReadFile(providersLockFile) + if err != nil { + t.Fatalf("failed to read providers lock file %s: %s", providersLockFile, err) + } + // The hash in here is for the empty files that mockGetProvider produces + wantLockFile := strings.TrimSpace(` +{ + "test": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +} +`) + if string(buf) != wantLockFile { + t.Errorf("wrong provider lock file contents\ngot: %s\nwant: %s", buf, wantLockFile) + } +} + /* func TestInit_remoteState(t *testing.T) { tmp, cwd := testCwd(t) diff --git a/command/plugins.go b/command/plugins.go index 020e839466..9539ef51df 100644 --- a/command/plugins.go +++ b/command/plugins.go @@ -23,17 +23,27 @@ type multiVersionProviderResolver struct { Available discovery.PluginMetaSet } +func choosePlugins(avail discovery.PluginMetaSet, reqd discovery.PluginRequirements) map[string]discovery.PluginMeta { + candidates := avail.ConstrainVersions(reqd) + ret := map[string]discovery.PluginMeta{} + for name, metas := range candidates { + if len(metas) == 0 { + continue + } + ret[name] = metas.Newest() + } + return ret +} + func (r *multiVersionProviderResolver) ResolveProviders( reqd discovery.PluginRequirements, ) (map[string]terraform.ResourceProviderFactory, []error) { factories := make(map[string]terraform.ResourceProviderFactory, len(reqd)) var errs []error - candidates := r.Available.ConstrainVersions(reqd) + chosen := choosePlugins(r.Available, reqd) for name := range reqd { - if metas := candidates[name]; metas != nil { - newest := metas.Newest() - + if newest, available := chosen[name]; available { digest, err := newest.SHA256() if err != nil { errs = append(errs, fmt.Errorf("provider.%s: failed to load plugin to verify its signature: %s", name, err)) @@ -45,7 +55,7 @@ func (r *multiVersionProviderResolver) ResolveProviders( // here is that they need to run "terraform init" to // fix this, which is covered by the UI code reporting these // error messages. - errs = append(errs, fmt.Errorf("provider.%s: not yet initialized", name)) + errs = append(errs, fmt.Errorf("provider.%s: installed but not yet initialized", name)) continue } @@ -108,10 +118,10 @@ func (m *Meta) providerResolver() terraform.ResourceProviderResolver { } // filter the requirements returning only the providers that we can't resolve -func (m *Meta) missingProviders(reqd discovery.PluginRequirements) discovery.PluginRequirements { +func (m *Meta) missingPlugins(avail discovery.PluginMetaSet, reqd discovery.PluginRequirements) discovery.PluginRequirements { missing := make(discovery.PluginRequirements) - candidates := m.providerPluginSet().ConstrainVersions(reqd) + candidates := avail.ConstrainVersions(reqd) for name, versionSet := range reqd { if metas := candidates[name]; metas == nil { diff --git a/command/push_test.go b/command/push_test.go index 473a31843d..ec83abaa39 100644 --- a/command/push_test.go +++ b/command/push_test.go @@ -4,10 +4,12 @@ import ( "archive/tar" "bytes" "compress/gzip" + "fmt" "io" "os" "path/filepath" "reflect" + "runtime" "sort" "strings" "testing" @@ -121,6 +123,9 @@ func TestPush_goodBackendInit(t *testing.T) { // Expected weird behavior, doesn't affect unpackaging ".terraform/", ".terraform/", + ".terraform/plugins/", + fmt.Sprintf(".terraform/plugins/%s_%s/", runtime.GOOS, runtime.GOARCH), + fmt.Sprintf(".terraform/plugins/%s_%s/providers.json", runtime.GOOS, runtime.GOARCH), ".terraform/terraform.tfstate", ".terraform/terraform.tfstate", "main.tf", diff --git a/command/test-fixtures/init-provider-lock-file/main.tf b/command/test-fixtures/init-provider-lock-file/main.tf new file mode 100644 index 0000000000..7eed7c5613 --- /dev/null +++ b/command/test-fixtures/init-provider-lock-file/main.tf @@ -0,0 +1,3 @@ +provider "test" { + version = "1.2.3" +} diff --git a/command/test-fixtures/init-providers-lock/main.tf b/command/test-fixtures/init-providers-lock/main.tf new file mode 100644 index 0000000000..7eed7c5613 --- /dev/null +++ b/command/test-fixtures/init-providers-lock/main.tf @@ -0,0 +1,3 @@ +provider "test" { + version = "1.2.3" +}