| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package e2etest | |
| import ( | |
| "bytes" | |
| "fmt" | |
| "os" | |
| "path/filepath" | |
| "runtime" | |
| "strings" | |
| "testing" | |
| "github.com/google/go-cmp/cmp" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| "github.com/hashicorp/terraform/internal/e2e" | |
| "github.com/hashicorp/terraform/internal/getproviders" | |
| ) | |
| func TestInitProviders(t *testing.T) { | |
| t.Parallel() | |
| // This test reaches out to releases.hashicorp.com to download the | |
| // template provider, so it can only run if network access is allowed. | |
| // We intentionally don't try to stub this here, because there's already | |
| // a stubbed version of this in the "command" package and so the goal here | |
| // is to test the interaction with the real repository. | |
| skipIfCannotAccessNetwork(t) | |
| fixturePath := filepath.Join("testdata", "template-provider") | |
| tf := e2e.NewBinary(t, terraformBin, fixturePath) | |
| // zero out any existing cli config file by passing in an empty file. | |
| configFile := emptyConfigFileForTests(t, tf.WorkDir()) | |
| tf.AddEnv(fmt.Sprintf("TF_CLI_CONFIG_FILE=%s", configFile)) | |
| 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, "- Installing hashicorp/template v") { | |
| t.Errorf("provider download message is missing from output:\n%s", stdout) | |
| t.Logf("(this can happen if you have a copy of the plugin in one of the global plugin search dirs)") | |
| } | |
| if !strings.Contains(stdout, "Terraform has created a lock file") { | |
| t.Errorf("lock file notification is missing from output:\n%s", stdout) | |
| } | |
| } | |
| 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("testdata", "terraform-provider") | |
| tf := e2e.NewBinary(t, terraformBin, fixturePath) | |
| // zero out any existing cli config file by passing in an empty file. | |
| configFile := emptyConfigFileForTests(t, tf.WorkDir()) | |
| tf.AddEnv(fmt.Sprintf("TF_CLI_CONFIG_FILE=%s", configFile)) | |
| 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, "Installing hashicorp/terraform") { | |
| // 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) | |
| } | |
| if strings.Contains(stdout, "Installing terraform.io/builtin/terraform") { | |
| // 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 TestInitProvidersVendored(t *testing.T) { | |
| t.Parallel() | |
| // This test will try to reach out to registry.terraform.io as one of the | |
| // possible installation locations for | |
| // hashicorp/null, where it will find that | |
| // versions do exist but will ultimately select the version that is | |
| // vendored due to the version constraint. | |
| skipIfCannotAccessNetwork(t) | |
| fixturePath := filepath.Join("testdata", "vendored-provider") | |
| tf := e2e.NewBinary(t, terraformBin, fixturePath) | |
| // Our fixture dir has a generic os_arch dir, which we need to customize | |
| // to the actual OS/arch where this test is running in order to get the | |
| // desired result. | |
| fixtMachineDir := tf.Path("terraform.d/plugins/registry.terraform.io/hashicorp/null/1.0.0+local/os_arch") | |
| wantMachineDir := tf.Path("terraform.d/plugins/registry.terraform.io/hashicorp/null/1.0.0+local/", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)) | |
| err := os.Rename(fixtMachineDir, wantMachineDir) | |
| if err != nil { | |
| t.Fatalf("unexpected error: %s", err) | |
| } | |
| // zero out any existing cli config file by passing in an empty file. | |
| configFile := emptyConfigFileForTests(t, tf.WorkDir()) | |
| tf.AddEnv(fmt.Sprintf("TF_CLI_CONFIG_FILE=%s", configFile)) | |
| 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, "- Installing hashicorp/null v1.0.0+local") { | |
| t.Errorf("provider download message is missing from output:\n%s", stdout) | |
| } | |
| } | |
| func TestInitProvidersLocalOnly(t *testing.T) { | |
| t.Parallel() | |
| // This test should not reach out to the network if it is behaving as | |
| // intended. If it _does_ try to access an upstream registry and encounter | |
| // an error doing so then that's a legitimate test failure that should be | |
| // fixed. (If it incorrectly reaches out anywhere then it's likely to be | |
| // to the host "example.com", which is the placeholder domain we use in | |
| // the test fixture.) | |
| fixturePath := filepath.Join("testdata", "local-only-provider") | |
| tf := e2e.NewBinary(t, terraformBin, fixturePath) | |
| // Our fixture dir has a generic os_arch dir, which we need to customize | |
| // to the actual OS/arch where this test is running in order to get the | |
| // desired result. | |
| fixtMachineDir := tf.Path("terraform.d/plugins/example.com/awesomecorp/happycloud/1.2.0/os_arch") | |
| wantMachineDir := tf.Path("terraform.d/plugins/example.com/awesomecorp/happycloud/1.2.0/", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)) | |
| err := os.Rename(fixtMachineDir, wantMachineDir) | |
| if err != nil { | |
| t.Fatalf("unexpected error: %s", err) | |
| } | |
| // If you run this test on a workstation with a plugin-cache directory | |
| // configured, it will leave a bad directory behind and terraform init will | |
| // not work until you remove it. | |
| // | |
| // To avoid this, we will "zero out" any existing cli config file by | |
| // passing in an empty override file. | |
| configFile := emptyConfigFileForTests(t, wantMachineDir) | |
| tf.AddEnv(fmt.Sprintf("TF_CLI_CONFIG_FILE=%s", configFile)) | |
| 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) | |
| t.Logf("(a \"Failed to query available provider packages\" error can happen here if you have a .terraformrc CLI configuration file present in your home directory)") | |
| } | |
| if !strings.Contains(stdout, "Terraform has been successfully initialized!") { | |
| t.Errorf("success message is missing from output:\n%s", stdout) | |
| } | |
| if !strings.Contains(stdout, "- Installing example.com/awesomecorp/happycloud v1.2.0") { | |
| t.Errorf("provider download message is missing from output:\n%s", stdout) | |
| t.Logf("(this can happen if you have a conflicting copy of the plugin in one of the global plugin search dirs)") | |
| } | |
| } | |
| func TestInitProvidersCustomMethod(t *testing.T) { | |
| t.Parallel() | |
| // This test should not reach out to the network if it is behaving as | |
| // intended. If it _does_ try to access an upstream registry and encounter | |
| // an error doing so then that's a legitimate test failure that should be | |
| // fixed. (If it incorrectly reaches out anywhere then it's likely to be | |
| // to the host "example.com", which is the placeholder domain we use in | |
| // the test fixture.) | |
| for _, configFile := range []string{"cliconfig.tfrc", "cliconfig.tfrc.json"} { | |
| t.Run(configFile, func(t *testing.T) { | |
| fixturePath := filepath.Join("testdata", "custom-provider-install-method") | |
| tf := e2e.NewBinary(t, terraformBin, fixturePath) | |
| // Our fixture dir has a generic os_arch dir, which we need to customize | |
| // to the actual OS/arch where this test is running in order to get the | |
| // desired result. | |
| fixtMachineDir := tf.Path("fs-mirror/example.com/awesomecorp/happycloud/1.2.0/os_arch") | |
| wantMachineDir := tf.Path("fs-mirror/example.com/awesomecorp/happycloud/1.2.0/", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)) | |
| err := os.Rename(fixtMachineDir, wantMachineDir) | |
| if err != nil { | |
| t.Fatalf("unexpected error: %s", err) | |
| } | |
| // We'll use a local CLI configuration file taken from our fixture | |
| // directory so we can force a custom installation method config. | |
| tf.AddEnv("TF_CLI_CONFIG_FILE=" + tf.Path(configFile)) | |
| 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, "- Installing example.com/awesomecorp/happycloud v1.2.0") { | |
| t.Errorf("provider download message is missing from output:\n%s", stdout) | |
| } | |
| }) | |
| } | |
| } | |
| func TestInitProviders_pluginCache(t *testing.T) { | |
| t.Parallel() | |
| // This test reaches out to releases.hashicorp.com to access plugin | |
| // metadata, and download the null plugin, though the template plugin | |
| // should come from local cache. | |
| skipIfCannotAccessNetwork(t) | |
| fixturePath := filepath.Join("testdata", "plugin-cache") | |
| tf := e2e.NewBinary(t, terraformBin, fixturePath) | |
| // Our fixture dir has a generic os_arch dir, which we need to customize | |
| // to the actual OS/arch where this test is running in order to get the | |
| // desired result. | |
| fixtMachineDir := tf.Path("cache/registry.terraform.io/hashicorp/template/2.1.0/os_arch") | |
| wantMachineDir := tf.Path("cache/registry.terraform.io/hashicorp/template/2.1.0/", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)) | |
| err := os.Rename(fixtMachineDir, wantMachineDir) | |
| if err != nil { | |
| t.Fatalf("unexpected error: %s", err) | |
| } | |
| // zero out any existing cli config file by passing in an empty file. | |
| configFile := emptyConfigFileForTests(t, tf.WorkDir()) | |
| tf.AddEnv(fmt.Sprintf("TF_CLI_CONFIG_FILE=%s", configFile)) | |
| cmd := tf.Cmd("init") | |
| // convert the slashes if building for windows. | |
| p := filepath.FromSlash("./cache") | |
| cmd.Env = append(cmd.Env, "TF_PLUGIN_CACHE_DIR="+p) | |
| err = cmd.Run() | |
| if err != nil { | |
| t.Errorf("unexpected error: %s", err) | |
| } | |
| path := filepath.FromSlash(fmt.Sprintf(".terraform/providers/registry.terraform.io/hashicorp/template/2.1.0/%s_%s/terraform-provider-template_v2.1.0_x4", runtime.GOOS, runtime.GOARCH)) | |
| content, err := tf.ReadFile(path) | |
| if err != nil { | |
| t.Fatalf("failed to read installed plugin from %s: %s", path, err) | |
| } | |
| if strings.TrimSpace(string(content)) != "this is not a real plugin" { | |
| t.Errorf("template plugin was not installed from local cache") | |
| } | |
| nullLinkPath := filepath.FromSlash(fmt.Sprintf(".terraform/providers/registry.terraform.io/hashicorp/null/2.1.0/%s_%s/terraform-provider-null_v2.1.0_x4", runtime.GOOS, runtime.GOARCH)) | |
| if runtime.GOOS == "windows" { | |
| nullLinkPath = nullLinkPath + ".exe" | |
| } | |
| if !tf.FileExists(nullLinkPath) { | |
| t.Errorf("null plugin was not installed into %s", nullLinkPath) | |
| } | |
| nullCachePath := filepath.FromSlash(fmt.Sprintf("cache/registry.terraform.io/hashicorp/null/2.1.0/%s_%s/terraform-provider-null_v2.1.0_x4", runtime.GOOS, runtime.GOARCH)) | |
| if runtime.GOOS == "windows" { | |
| nullCachePath = nullCachePath + ".exe" | |
| } | |
| if !tf.FileExists(nullCachePath) { | |
| t.Errorf("null plugin is not in cache after install. expected in: %s", nullCachePath) | |
| } | |
| } | |
| func TestInit_fromModule(t *testing.T) { | |
| t.Parallel() | |
| // This test reaches out to registry.terraform.io and github.com to lookup | |
| // and fetch a module. | |
| skipIfCannotAccessNetwork(t) | |
| fixturePath := filepath.Join("testdata", "empty") | |
| tf := e2e.NewBinary(t, terraformBin, fixturePath) | |
| cmd := tf.Cmd("init", "-from-module=hashicorp/vault-starter/aws") | |
| cmd.Stdin = nil | |
| cmd.Stderr = &bytes.Buffer{} | |
| err := cmd.Run() | |
| if err != nil { | |
| t.Errorf("unexpected error: %s", err) | |
| } | |
| stderr := cmd.Stderr.(*bytes.Buffer).String() | |
| if stderr != "" { | |
| t.Errorf("unexpected stderr output:\n%s", stderr) | |
| } | |
| content, err := tf.ReadFile("main.tf") | |
| if err != nil { | |
| t.Fatalf("failed to read main.tf: %s", err) | |
| } | |
| if !bytes.Contains(content, []byte("vault")) { | |
| t.Fatalf("main.tf doesn't appear to be a vault configuration: \n%s", content) | |
| } | |
| } | |
| func TestInitProviderNotFound(t *testing.T) { | |
| t.Parallel() | |
| // This test will reach out to registry.terraform.io as one of the possible | |
| // installation locations for hashicorp/nonexist, which should not exist. | |
| skipIfCannotAccessNetwork(t) | |
| fixturePath := filepath.Join("testdata", "provider-not-found") | |
| tf := e2e.NewBinary(t, terraformBin, fixturePath) | |
| t.Run("registry provider not found", func(t *testing.T) { | |
| _, stderr, err := tf.Run("init", "-no-color") | |
| if err == nil { | |
| t.Fatal("expected error, got success") | |
| } | |
| oneLineStderr := strings.ReplaceAll(stderr, "\n", " ") | |
| if !strings.Contains(oneLineStderr, "provider registry registry.terraform.io does not have a provider named registry.terraform.io/hashicorp/nonexist") { | |
| t.Errorf("expected error message is missing from output:\n%s", stderr) | |
| } | |
| if !strings.Contains(oneLineStderr, "All modules should specify their required_providers") { | |
| t.Errorf("expected error message is missing from output:\n%s", stderr) | |
| } | |
| }) | |
| t.Run("local provider not found", func(t *testing.T) { | |
| // The -plugin-dir directory must exist for the provider installer to search it. | |
| pluginDir := tf.Path("empty") | |
| if err := os.Mkdir(pluginDir, os.ModePerm); err != nil { | |
| t.Fatal(err) | |
| } | |
| _, stderr, err := tf.Run("init", "-no-color", "-plugin-dir="+pluginDir) | |
| if err == nil { | |
| t.Fatal("expected error, got success") | |
| } | |
| if !strings.Contains(stderr, "provider registry.terraform.io/hashicorp/nonexist was not\nfound in any of the search locations\n\n - "+pluginDir) { | |
| t.Errorf("expected error message is missing from output:\n%s", stderr) | |
| } | |
| }) | |
| t.Run("special characters enabled", func(t *testing.T) { | |
| _, stderr, err := tf.Run("init") | |
| if err == nil { | |
| t.Fatal("expected error, got success") | |
| } | |
| expectedErr := `╷ | |
| │ Error: Failed to query available provider packages | |
| │` + ` ` + ` | |
| │ Could not retrieve the list of available versions for provider | |
| │ hashicorp/nonexist: provider registry registry.terraform.io does not have a | |
| │ provider named registry.terraform.io/hashicorp/nonexist | |
| │` + ` ` + ` | |
| │ All modules should specify their required_providers so that external | |
| │ consumers will get the correct providers when using a module. To see which | |
| │ modules are currently depending on hashicorp/nonexist, run the following | |
| │ command: | |
| │ terraform providers | |
| ╵ | |
| ` | |
| if stripAnsi(stderr) != expectedErr { | |
| t.Errorf("wrong output:\n%s", cmp.Diff(stripAnsi(stderr), expectedErr)) | |
| } | |
| }) | |
| } | |
| func TestInitProviderWarnings(t *testing.T) { | |
| t.Parallel() | |
| // This test will reach out to registry.terraform.io as one of the possible | |
| // installation locations for hashicorp/nonexist, which should not exist. | |
| skipIfCannotAccessNetwork(t) | |
| fixturePath := filepath.Join("testdata", "provider-warnings") | |
| tf := e2e.NewBinary(t, terraformBin, fixturePath) | |
| stdout, _, err := tf.Run("init") | |
| if err == nil { | |
| t.Fatal("expected error, got success") | |
| } | |
| if !strings.Contains(stdout, "This provider is archived and no longer needed.") { | |
| t.Errorf("expected warning message is missing from output:\n%s", stdout) | |
| } | |
| } | |
| // This is a regression test asserting that `terraform init -upgrade` doesn't error | |
| // when the state storage provider is unmanaged by Terraform. The check to see if the | |
| // state storage provider was affected by the upgrade process should be skipped when | |
| // the provider is not managed by Terraform. Previously the check wasn't skipped and | |
| // panicked due to how the provider was supplied. | |
| // | |
| // See the TestInit_getUpgradePlugins integration test for similar testing when using a | |
| // dev_override provider. | |
| func TestInitStateStoreUsingUnmanagedProvider(t *testing.T) { | |
| if !canRunGoBuild { | |
| // We're running in a separate-build-then-run context, so we can't | |
| // currently execute this test which depends on being able to build | |
| // new executable at runtime. | |
| // | |
| // (See the comment on canRunGoBuild's declaration for more information.) | |
| t.Skip("can't run without building a new provider executable") | |
| } | |
| // In temp dir create a plugin cache to be used in the test cases. | |
| // The cache is supplied to commands using the -plugin-dir init flag. | |
| // There are 2 versions of the simple6 provider: 0.0.1 and 2.0.0 | |
| // This enables us to test an upgrade scenario where a newer provider version is available. | |
| td := t.TempDir() | |
| providerVersionOld := "0.0.1" | |
| providerVersionNew := "2.0.0" | |
| platform := getproviders.CurrentPlatform.String() | |
| absolutePathToCache := filepath.Join(td, "cache") | |
| simple6Provider := filepath.Join(td, "terraform-provider-simple6") | |
| simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider) | |
| for _, v := range []string{providerVersionOld, providerVersionNew} { | |
| dir := filepath.Join(absolutePathToCache, "registry.terraform.io/hashicorp", "simple6", v, platform) | |
| if err := os.MkdirAll(dir, os.ModePerm); err != nil { | |
| t.Fatal(err) | |
| } | |
| // Create an executable copy of the simple6ProviderExe file per version in the cache dir | |
| data, err := os.ReadFile(simple6ProviderExe) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if err := os.WriteFile(filepath.Join(dir, "terraform-provider-simple6"), data, 0755); err != nil { | |
| t.Fatal(err) | |
| } | |
| } | |
| fixturePath := filepath.Join("testdata", "initialized-directory-with-state-store-unmanaged") | |
| tf := e2e.NewBinary(t, experimentalTerraformBin, fixturePath) | |
| // Assert the existing lockfile describes the older version of the provider. | |
| lockFile := tf.Path(".terraform.lock.hcl") | |
| buf, err := os.ReadFile(lockFile) | |
| if err != nil { | |
| t.Fatalf("unexpected error accessing lock file: %s", err) | |
| } | |
| buf = bytes.TrimSpace(buf) | |
| expectedLockFileContent := fmt.Sprintf(`# This file is maintained automatically by "terraform init". | |
| # Manual edits may be lost in future updates. | |
| provider "registry.terraform.io/hashicorp/simple6" { | |
| version = "%s" | |
| }`, providerVersionOld) | |
| if diff := cmp.Diff(expectedLockFileContent, string(buf)); diff != "" { | |
| t.Errorf("unexpected difference in lock file content: %s", diff) | |
| } | |
| // The simple6 provider is unmanaged | |
| reattachConfig, _ := reattachedProviderForTest(t, addrs.NewDefaultProvider("simple6"), 6) | |
| tf.AddEnv("TF_REATTACH_PROVIDERS=" + reattachConfig) | |
| // The init -upgrade process should succeed. | |
| stdout, stderr, err := tf.Run( | |
| "init", | |
| "-upgrade", | |
| "-enable-pluggable-state-storage-experiment", | |
| fmt.Sprintf("-plugin-dir=%s", absolutePathToCache), | |
| ) | |
| if err != nil { | |
| t.Fatalf("unexpected error: %s\nstdout: %s\nstderr: %s", err, stdout, stderr) | |
| } | |
| // Lockfile should be unchanged because the only provider in the config is unmanaged. | |
| buf, err = os.ReadFile(lockFile) | |
| if err != nil { | |
| t.Fatalf("unexpected error accessing lock file: %s", err) | |
| } | |
| buf = bytes.TrimSpace(buf) | |
| if diff := cmp.Diff(expectedLockFileContent, string(buf)); diff != "" { | |
| t.Errorf("unexpected difference in lock file content: %s", diff) | |
| } | |
| } | |
| // emptyConfigFileForTests creates a blank .terraformrc file in the requested | |
| // path and returns the path to the new file. It is the caller's responsibility | |
| // to cleanup the file after use. | |
| func emptyConfigFileForTests(t testing.TB, path string) string { | |
| // zero out any existing cli config file by passing in an empty file. | |
| configFile, err := os.Create(filepath.Join(path, ".terraformrc")) | |
| if err != nil { | |
| t.Fatalf("unexpected error: %s", err) | |
| } | |
| configFile.Close() | |
| return configFile.Name() | |
| } |