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.
This commit is contained in:
Martin Atkins 2017-05-24 17:35:46 -07:00
parent 04bcece59c
commit 032f71f1ff
6 changed files with 98 additions and 8 deletions

View File

@ -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
}

View File

@ -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)

View File

@ -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 {

View File

@ -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",

View File

@ -0,0 +1,3 @@
provider "test" {
version = "1.2.3"
}

View File

@ -0,0 +1,3 @@
provider "test" {
version = "1.2.3"
}