2021-10-01 18:19:36 -05:00
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
2021-12-23 11:10:59 -06:00
pluginDir := filepath . Join ( ".terraform" , "providers" , "registry.terraform.io" , "hashicorp" , "null" , providerVersion , getproviders . CurrentPlatform . String ( ) )
pluginExe := filepath . Join ( pluginDir , "terraform-provider-null_v" + providerVersion + "_x5" )
2021-10-01 18:19:36 -05:00
if getproviders . CurrentPlatform . OS == "windows" {
pluginExe += ".exe" // ugh
}
2021-12-23 11:10:59 -06:00
// 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" )
2021-10-01 18:19:36 -05:00
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 )
}
2021-10-21 07:41:01 -05:00
stdout , stderr , err := tf . Run ( "plan" )
2021-10-01 18:19:36 -05:00
if err == nil {
t . Fatalf ( "unexpected plan success\nstdout:\n%s" , stdout )
}
2021-12-23 11:10:59 -06:00
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 ) {
2021-10-01 18:19:36 -05:00
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 )
}
2021-10-21 07:41:01 -05:00
// 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 )
}
2021-10-01 18:19:36 -05:00
} )
2021-10-25 10:28:02 -05:00
t . Run ( "cache dir totally gone, explicit backend" , func ( t * testing . T ) {
tf := e2e . NewBinary ( terraformBin , seedDir )
defer tf . Close ( )
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 )
}
} )
2021-10-01 18:19:36 -05:00
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 )
}
2021-12-23 11:10:59 -06:00
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 ) {
2021-10-01 18:19:36 -05:00
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 )
}
2021-12-23 11:10:59 -06:00
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 ) {
2021-10-01 18:19:36 -05:00
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 )
}
2021-12-23 11:10:59 -06:00
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 ) {
2021-10-01 18:19:36 -05:00
t . Errorf ( "missing expected error message\nwant substring: %s\ngot:\n%s" , want , stderr )
}
} )
}
2021-10-25 10:28:02 -05:00
const localBackendConfig = `
terraform {
backend "local" {
path = "terraform.tfstate"
}
}
`