| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package e2etest |
| |
| import ( |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "strings" |
| "testing" |
| |
| "github.com/hashicorp/terraform/internal/e2e" |
| "github.com/hashicorp/terraform/internal/getproviders" |
| ) |
| |
| // TestProviderTampering tests various ways that the provider plugins in the |
| // local cache directory might be modified after an initial "terraform init", |
| // which other Terraform commands which use those plugins should catch and |
| // report early. |
| func TestProviderTampering(t *testing.T) { |
| // General setup: we'll do a one-off init of a test directory as our |
| // starting point, and then we'll clone that result for each test so |
| // that we can save the cost of a repeated re-init with the same |
| // provider. |
| t.Parallel() |
| |
| // This test reaches out to releases.hashicorp.com to download the |
| // null provider, so it can only run if network access is allowed. |
| skipIfCannotAccessNetwork(t) |
| |
| fixturePath := filepath.Join("testdata", "provider-tampering-base") |
| tf := e2e.NewBinary(t, terraformBin, fixturePath) |
| |
| stdout, stderr, err := tf.Run("init") |
| if err != nil { |
| t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr) |
| } |
| if !strings.Contains(stdout, "Installing hashicorp/null v") { |
| t.Errorf("null provider download message is missing from init 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)") |
| } |
| |
| seedDir := tf.WorkDir() |
| const providerVersion = "3.1.0" // must match the version in the fixture config |
| pluginDir := filepath.Join(".terraform", "providers", "registry.terraform.io", "hashicorp", "null", providerVersion, getproviders.CurrentPlatform.String()) |
| pluginExe := filepath.Join(pluginDir, "terraform-provider-null_v"+providerVersion+"_x5") |
| if getproviders.CurrentPlatform.OS == "windows" { |
| pluginExe += ".exe" // ugh |
| } |
| |
| // filepath.Join here to make sure we get the right path separator |
| // for whatever OS we're running these tests on. |
| providerCacheDir := filepath.Join(".terraform", "providers") |
| |
| t.Run("cache dir totally gone", func(t *testing.T) { |
| tf := e2e.NewBinary(t, terraformBin, seedDir) |
| workDir := tf.WorkDir() |
| |
| err := os.RemoveAll(filepath.Join(workDir, ".terraform")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| stdout, stderr, err := tf.Run("plan") |
| if err == nil { |
| t.Fatalf("unexpected plan success\nstdout:\n%s", stdout) |
| } |
| if want := `registry.terraform.io/hashicorp/null: there is no package for registry.terraform.io/hashicorp/null 3.1.0 cached in ` + providerCacheDir; !strings.Contains(stderr, want) { |
| t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr) |
| } |
| if want := `terraform init`; !strings.Contains(stderr, want) { |
| t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr) |
| } |
| |
| // Running init as suggested resolves the problem |
| _, stderr, err = tf.Run("init") |
| if err != nil { |
| t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr) |
| } |
| _, stderr, err = tf.Run("plan") |
| if err != nil { |
| t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr) |
| } |
| }) |
| t.Run("cache dir totally gone, explicit backend", func(t *testing.T) { |
| tf := e2e.NewBinary(t, terraformBin, seedDir) |
| workDir := tf.WorkDir() |
| |
| err := ioutil.WriteFile(filepath.Join(workDir, "backend.tf"), []byte(localBackendConfig), 0600) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| err = os.RemoveAll(filepath.Join(workDir, ".terraform")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| stdout, stderr, err := tf.Run("plan") |
| if err == nil { |
| t.Fatalf("unexpected plan success\nstdout:\n%s", stdout) |
| } |
| if want := `Initial configuration of the requested backend "local"`; !strings.Contains(stderr, want) { |
| t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr) |
| } |
| if want := `terraform init`; !strings.Contains(stderr, want) { |
| t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr) |
| } |
| |
| // Running init as suggested resolves the problem |
| _, stderr, err = tf.Run("init") |
| if err != nil { |
| t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr) |
| } |
| _, stderr, err = tf.Run("plan") |
| if err != nil { |
| t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr) |
| } |
| }) |
| t.Run("null plugin package modified before plan", func(t *testing.T) { |
| tf := e2e.NewBinary(t, terraformBin, seedDir) |
| workDir := tf.WorkDir() |
| |
| err := ioutil.WriteFile(filepath.Join(workDir, pluginExe), []byte("tamper"), 0600) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| stdout, stderr, err := tf.Run("plan") |
| if err == nil { |
| t.Fatalf("unexpected plan success\nstdout:\n%s", stdout) |
| } |
| if want := `registry.terraform.io/hashicorp/null: the cached package for registry.terraform.io/hashicorp/null 3.1.0 (in ` + providerCacheDir + `) does not match any of the checksums recorded in the dependency lock file`; !strings.Contains(stderr, want) { |
| t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr) |
| } |
| if want := `terraform init`; !strings.Contains(stderr, want) { |
| t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr) |
| } |
| }) |
| t.Run("version constraint changed in config before plan", func(t *testing.T) { |
| tf := e2e.NewBinary(t, terraformBin, seedDir) |
| workDir := tf.WorkDir() |
| |
| err := ioutil.WriteFile(filepath.Join(workDir, "provider-tampering-base.tf"), []byte(` |
| terraform { |
| required_providers { |
| null = { |
| source = "hashicorp/null" |
| version = "1.0.0" |
| } |
| } |
| } |
| `), 0600) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| stdout, stderr, err := tf.Run("plan") |
| if err == nil { |
| t.Fatalf("unexpected plan success\nstdout:\n%s", stdout) |
| } |
| if want := `provider registry.terraform.io/hashicorp/null: locked version selection 3.1.0 doesn't match the updated version constraints "1.0.0"`; !strings.Contains(stderr, want) { |
| t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr) |
| } |
| if want := `terraform init -upgrade`; !strings.Contains(stderr, want) { |
| t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr) |
| } |
| }) |
| t.Run("lock file modified before plan", func(t *testing.T) { |
| tf := e2e.NewBinary(t, terraformBin, seedDir) |
| workDir := tf.WorkDir() |
| |
| // NOTE: We're just emptying out the lock file here because that's |
| // good enough for what we're trying to assert. The leaf codepath |
| // that generates this family of errors has some different variations |
| // of this error message for otehr sorts of inconsistency, but those |
| // are tested more thoroughly over in the "configs" package, which is |
| // ultimately responsible for that logic. |
| err := ioutil.WriteFile(filepath.Join(workDir, ".terraform.lock.hcl"), []byte(``), 0600) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| stdout, stderr, err := tf.Run("plan") |
| if err == nil { |
| t.Fatalf("unexpected plan success\nstdout:\n%s", stdout) |
| } |
| if want := `provider registry.terraform.io/hashicorp/null: required by this configuration but no version is selected`; !strings.Contains(stderr, want) { |
| t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr) |
| } |
| if want := `terraform init`; !strings.Contains(stderr, want) { |
| t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr) |
| } |
| }) |
| t.Run("lock file modified after plan", func(t *testing.T) { |
| tf := e2e.NewBinary(t, terraformBin, seedDir) |
| workDir := tf.WorkDir() |
| |
| _, stderr, err := tf.Run("plan", "-out", "tfplan") |
| if err != nil { |
| t.Fatalf("unexpected plan failure\nstderr:\n%s", stderr) |
| } |
| |
| err = os.Remove(filepath.Join(workDir, ".terraform.lock.hcl")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| stdout, stderr, err := tf.Run("apply", "tfplan") |
| if err == nil { |
| t.Fatalf("unexpected apply success\nstdout:\n%s", stdout) |
| } |
| if want := `provider registry.terraform.io/hashicorp/null: required by this configuration but no version is selected`; !strings.Contains(stderr, want) { |
| t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr) |
| } |
| if want := `Create a new plan from the updated configuration.`; !strings.Contains(stderr, want) { |
| t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr) |
| } |
| }) |
| t.Run("plugin cache dir entirely removed after plan", func(t *testing.T) { |
| tf := e2e.NewBinary(t, terraformBin, seedDir) |
| workDir := tf.WorkDir() |
| |
| _, stderr, err := tf.Run("plan", "-out", "tfplan") |
| if err != nil { |
| t.Fatalf("unexpected plan failure\nstderr:\n%s", stderr) |
| } |
| |
| err = os.RemoveAll(filepath.Join(workDir, ".terraform")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| stdout, stderr, err := tf.Run("apply", "tfplan") |
| if err == nil { |
| t.Fatalf("unexpected apply success\nstdout:\n%s", stdout) |
| } |
| if want := `registry.terraform.io/hashicorp/null: there is no package for registry.terraform.io/hashicorp/null 3.1.0 cached in ` + providerCacheDir; !strings.Contains(stderr, want) { |
| t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr) |
| } |
| }) |
| t.Run("null plugin package modified after plan", func(t *testing.T) { |
| tf := e2e.NewBinary(t, terraformBin, seedDir) |
| workDir := tf.WorkDir() |
| |
| _, stderr, err := tf.Run("plan", "-out", "tfplan") |
| if err != nil { |
| t.Fatalf("unexpected plan failure\nstderr:\n%s", stderr) |
| } |
| |
| err = ioutil.WriteFile(filepath.Join(workDir, pluginExe), []byte("tamper"), 0600) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| stdout, stderr, err := tf.Run("apply", "tfplan") |
| if err == nil { |
| t.Fatalf("unexpected apply success\nstdout:\n%s", stdout) |
| } |
| if want := `registry.terraform.io/hashicorp/null: the cached package for registry.terraform.io/hashicorp/null 3.1.0 (in ` + providerCacheDir + `) does not match any of the checksums recorded in the dependency lock file`; !strings.Contains(stderr, want) { |
| t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr) |
| } |
| }) |
| } |
| |
| const localBackendConfig = ` |
| terraform { |
| backend "local" { |
| path = "terraform.tfstate" |
| } |
| } |
| ` |