diff --git a/command/e2etest/init_test.go b/command/e2etest/init_test.go index f14bf41bb..a80ba7db3 100644 --- a/command/e2etest/init_test.go +++ b/command/e2etest/init_test.go @@ -50,6 +50,37 @@ func TestInitProviders(t *testing.T) { } +func TestInitProvidersInternal(t *testing.T) { + t.Parallel() + + // This test should _not_ reach out anywhere because the "terraform" + // provider is internal to the core terraform binary. + + fixturePath := filepath.Join("test-fixtures", "terraform-provider") + tf := e2e.NewBinary(terraformBin, fixturePath) + defer tf.Close() + + stdout, stderr, err := tf.Run("init") + if err != nil { + t.Errorf("unexpected error: %s", err) + } + + if stderr != "" { + t.Errorf("unexpected stderr output:\n%s", stderr) + } + + if !strings.Contains(stdout, "Terraform has been successfully initialized!") { + t.Errorf("success message is missing from output:\n%s", stdout) + } + + if strings.Contains(stdout, "Downloading plugin for provider") { + // Shouldn't have downloaded anything with this config, because the + // provider is built in. + t.Errorf("provider download message appeared in output:\n%s", stdout) + } + +} + func TestInitProviders_pluginCache(t *testing.T) { t.Parallel() diff --git a/command/e2etest/test-fixtures/terraform-provider/main.tf b/command/e2etest/test-fixtures/terraform-provider/main.tf new file mode 100644 index 000000000..a8e222fca --- /dev/null +++ b/command/e2etest/test-fixtures/terraform-provider/main.tf @@ -0,0 +1,10 @@ +provider "terraform" { + +} + +data "terraform_remote_state" "test" { + backend = "local" + config = { + path = "nothing.tfstate" + } +} diff --git a/command/init.go b/command/init.go index 03e7df9ca..39eef877b 100644 --- a/command/init.go +++ b/command/init.go @@ -310,6 +310,7 @@ func (c *InitCommand) getProviders(path string, state *terraform.State, upgrade )) missing := c.missingPlugins(available, requirements) + internal := c.internalProviders() var errs error if c.getPlugins { @@ -319,6 +320,12 @@ func (c *InitCommand) getProviders(path string, state *terraform.State, upgrade } for provider, reqd := range missing { + if _, isInternal := internal[provider]; isInternal { + // Ignore internal providers; they are not eligible for + // installation. + continue + } + _, err := c.providerInstaller.Get(provider, reqd.Versions) if err != nil { @@ -376,7 +383,7 @@ func (c *InitCommand) getProviders(path string, state *terraform.State, upgrade // 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) + chosen := choosePlugins(available, internal, requirements) digests := map[string][]byte{} for name, meta := range chosen { digest, err := meta.SHA256() diff --git a/command/plugins.go b/command/plugins.go index ba034a2a9..5eba6b698 100644 --- a/command/plugins.go +++ b/command/plugins.go @@ -13,6 +13,7 @@ import ( "strings" plugin "github.com/hashicorp/go-plugin" + terraformProvider "github.com/hashicorp/terraform/builtin/providers/terraform" tfplugin "github.com/hashicorp/terraform/plugin" "github.com/hashicorp/terraform/plugin/discovery" "github.com/hashicorp/terraform/terraform" @@ -25,12 +26,25 @@ import ( // each that satisfies the given constraints. type multiVersionProviderResolver struct { Available discovery.PluginMetaSet + + // Internal is a map that overrides the usual plugin selection process + // for internal plugins. These plugins do not support version constraints + // (will produce an error if one is set). This should be used only in + // exceptional circumstances since it forces the provider's release + // schedule to be tied to that of Terraform Core. + Internal map[string]terraform.ResourceProviderFactory } -func choosePlugins(avail discovery.PluginMetaSet, reqd discovery.PluginRequirements) map[string]discovery.PluginMeta { +func choosePlugins(avail discovery.PluginMetaSet, internal map[string]terraform.ResourceProviderFactory, reqd discovery.PluginRequirements) map[string]discovery.PluginMeta { candidates := avail.ConstrainVersions(reqd) ret := map[string]discovery.PluginMeta{} for name, metas := range candidates { + // If the provider is in our internal map then we ignore any + // discovered plugins for it since these are dealt with separately. + if _, isInternal := internal[name]; isInternal { + continue + } + if len(metas) == 0 { continue } @@ -45,8 +59,17 @@ func (r *multiVersionProviderResolver) ResolveProviders( factories := make(map[string]terraform.ResourceProviderFactory, len(reqd)) var errs []error - chosen := choosePlugins(r.Available, reqd) + chosen := choosePlugins(r.Available, r.Internal, reqd) for name, req := range reqd { + if factory, isInternal := r.Internal[name]; isInternal { + if !req.Versions.Unconstrained() { + errs = append(errs, fmt.Errorf("provider.%s: this provider is built in to Terraform and so it does not support version constraints", name)) + continue + } + factories[name] = factory + continue + } + if newest, available := chosen[name]; available { digest, err := newest.SHA256() if err != nil { @@ -233,6 +256,15 @@ func (m *Meta) providerPluginManuallyInstalledSet() discovery.PluginMetaSet { func (m *Meta) providerResolver() terraform.ResourceProviderResolver { return &multiVersionProviderResolver{ Available: m.providerPluginSet(), + Internal: m.internalProviders(), + } +} + +func (m *Meta) internalProviders() map[string]terraform.ResourceProviderFactory { + return map[string]terraform.ResourceProviderFactory{ + "terraform": func() (terraform.ResourceProvider, error) { + return terraformProvider.Provider(), nil + }, } } diff --git a/command/plugins_test.go b/command/plugins_test.go index 7c7200ae5..fb7ee16e0 100644 --- a/command/plugins_test.go +++ b/command/plugins_test.go @@ -9,8 +9,91 @@ import ( "testing" "github.com/hashicorp/terraform/plugin/discovery" + "github.com/hashicorp/terraform/terraform" ) +func TestMultiVersionProviderResolver(t *testing.T) { + available := make(discovery.PluginMetaSet) + available.Add(discovery.PluginMeta{ + Name: "plugin", + Version: "1.0.0", + Path: "test-fixtures/empty-file", + }) + + resolver := &multiVersionProviderResolver{ + Internal: map[string]terraform.ResourceProviderFactory{ + "internal": func() (terraform.ResourceProvider, error) { + return &terraform.MockResourceProvider{ + ResourcesReturn: []terraform.ResourceType{ + { + Name: "internal_foo", + }, + }, + }, nil + }, + }, + Available: available, + } + + t.Run("plugin matches", func(t *testing.T) { + reqd := discovery.PluginRequirements{ + "plugin": &discovery.PluginConstraints{ + Versions: discovery.ConstraintStr("1.0.0").MustParse(), + }, + } + got, err := resolver.ResolveProviders(reqd) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if ct := len(got); ct != 1 { + t.Errorf("wrong number of results %d; want 1", ct) + } + if _, exists := got["plugin"]; !exists { + t.Errorf("provider \"plugin\" not in result") + } + }) + t.Run("plugin doesn't match", func(t *testing.T) { + reqd := discovery.PluginRequirements{ + "plugin": &discovery.PluginConstraints{ + Versions: discovery.ConstraintStr("2.0.0").MustParse(), + }, + } + _, err := resolver.ResolveProviders(reqd) + if err == nil { + t.Errorf("resolved successfully, but want error") + } + }) + t.Run("internal matches", func(t *testing.T) { + reqd := discovery.PluginRequirements{ + "internal": &discovery.PluginConstraints{ + Versions: discovery.AllVersions, + }, + } + got, err := resolver.ResolveProviders(reqd) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if ct := len(got); ct != 1 { + t.Errorf("wrong number of results %d; want 1", ct) + } + if _, exists := got["internal"]; !exists { + t.Errorf("provider \"internal\" not in result") + } + }) + t.Run("internal with version constraint", func(t *testing.T) { + // Version constraints are not permitted for internal providers + reqd := discovery.PluginRequirements{ + "internal": &discovery.PluginConstraints{ + Versions: discovery.ConstraintStr("2.0.0").MustParse(), + }, + } + _, err := resolver.ResolveProviders(reqd) + if err == nil { + t.Errorf("resolved successfully, but want error") + } + }) +} + func TestPluginPath(t *testing.T) { td, err := ioutil.TempDir("", "tf") if err != nil { @@ -36,6 +119,26 @@ func TestPluginPath(t *testing.T) { } } +func TestInternalProviders(t *testing.T) { + m := Meta{} + internal := m.internalProviders() + tfProvider, err := internal["terraform"]() + if err != nil { + t.Fatal(err) + } + + dataSources := tfProvider.DataSources() + found := false + for _, ds := range dataSources { + if ds.Name == "terraform_remote_state" { + found = true + } + } + if !found { + t.Errorf("didn't find terraform_remote_state in internal \"terraform\" provider") + } +} + // mockProviderInstaller is a discovery.PluginInstaller implementation that // is a mock for discovery.ProviderInstaller. type mockProviderInstaller struct { diff --git a/command/test-fixtures/empty-file b/command/test-fixtures/empty-file new file mode 100644 index 000000000..e69de29bb