mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-26 00:41:27 -06:00
command/e2etest: TestProviderTampering
We have various mechanisms that aim to ensure that the installed provider plugins are consistent with the lock file and that the lock file is consistent with the provider requirements, and we do have existing unit tests for them, but all of those cases mock our fake out at least part of the process and in the past that's caused us to miss usability regressions, where we still catch the error but do so at the wrong layer and thus generate error message lacking useful additional context. Here we'll add some new end-to-end tests to supplement the existing unit tests, making sure things work as expected when we assemble the system together as we would in a release. These tests cover a number of different ways in which the plugin selections can grow inconsistent. These new tests all run only when we're in a context where we're allowed to access the network, because they exercise the real plugin installer codepath. We could technically build this to use a local filesystem mirror or other such override to avoid that, but the point here is to make sure we see the expected behavior in the main case, and so it's worth the small additional cost of downloading the null provider from the real registry.
This commit is contained in:
parent
d09510a8fb
commit
01b22f4b76
221
internal/command/e2etest/providers_tamper_test.go
Normal file
221
internal/command/e2etest/providers_tamper_test.go
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
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(terraformBin, fixturePath)
|
||||||
|
defer tf.Close()
|
||||||
|
|
||||||
|
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 := ".terraform/providers/registry.terraform.io/hashicorp/null/" + providerVersion + "/" + getproviders.CurrentPlatform.String()
|
||||||
|
pluginExe := pluginDir + "/terraform-provider-null_v" + providerVersion + "_x5"
|
||||||
|
if getproviders.CurrentPlatform.OS == "windows" {
|
||||||
|
pluginExe += ".exe" // ugh
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("cache dir totally gone", func(t *testing.T) {
|
||||||
|
tf := e2e.NewBinary(terraformBin, seedDir)
|
||||||
|
defer tf.Close()
|
||||||
|
workDir := tf.WorkDir()
|
||||||
|
|
||||||
|
err := os.RemoveAll(filepath.Join(workDir, ".terraform"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, 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 .terraform/providers`; !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("null plugin package modified before plan", func(t *testing.T) {
|
||||||
|
tf := e2e.NewBinary(terraformBin, seedDir)
|
||||||
|
defer tf.Close()
|
||||||
|
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 .terraform/providers) 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(terraformBin, seedDir)
|
||||||
|
defer tf.Close()
|
||||||
|
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(terraformBin, seedDir)
|
||||||
|
defer tf.Close()
|
||||||
|
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(terraformBin, seedDir)
|
||||||
|
defer tf.Close()
|
||||||
|
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(terraformBin, seedDir)
|
||||||
|
defer tf.Close()
|
||||||
|
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 .terraform/providers`; !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(terraformBin, seedDir)
|
||||||
|
defer tf.Close()
|
||||||
|
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 .terraform/providers) 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
12
internal/command/e2etest/testdata/provider-tampering-base/provider-tampering-base.tf
vendored
Normal file
12
internal/command/e2etest/testdata/provider-tampering-base/provider-tampering-base.tf
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
null = {
|
||||||
|
# Our version is intentionally fixed so that we have a fixed
|
||||||
|
# test case here, though we might have to update this in future
|
||||||
|
# if e.g. Terraform stops supporting plugin protocol 5, or if
|
||||||
|
# the null provider is yanked from the registry for some reason.
|
||||||
|
source = "hashicorp/null"
|
||||||
|
version = "3.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user