github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/command/e2etest/providers_tamper_test.go (about)

     1  package e2etest
     2  
     3  import (
     4  	"io/ioutil"
     5  	"os"
     6  	"path/filepath"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/cycloidio/terraform/e2e"
    11  	"github.com/cycloidio/terraform/getproviders"
    12  )
    13  
    14  // TestProviderTampering tests various ways that the provider plugins in the
    15  // local cache directory might be modified after an initial "terraform init",
    16  // which other Terraform commands which use those plugins should catch and
    17  // report early.
    18  func TestProviderTampering(t *testing.T) {
    19  	// General setup: we'll do a one-off init of a test directory as our
    20  	// starting point, and then we'll clone that result for each test so
    21  	// that we can save the cost of a repeated re-init with the same
    22  	// provider.
    23  	t.Parallel()
    24  
    25  	// This test reaches out to releases.hashicorp.com to download the
    26  	// null provider, so it can only run if network access is allowed.
    27  	skipIfCannotAccessNetwork(t)
    28  
    29  	fixturePath := filepath.Join("testdata", "provider-tampering-base")
    30  	tf := e2e.NewBinary(terraformBin, fixturePath)
    31  	defer tf.Close()
    32  
    33  	stdout, stderr, err := tf.Run("init")
    34  	if err != nil {
    35  		t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
    36  	}
    37  	if !strings.Contains(stdout, "Installing hashicorp/null v") {
    38  		t.Errorf("null provider download message is missing from init output:\n%s", stdout)
    39  		t.Logf("(this can happen if you have a copy of the plugin in one of the global plugin search dirs)")
    40  	}
    41  
    42  	seedDir := tf.WorkDir()
    43  	const providerVersion = "3.1.0" // must match the version in the fixture config
    44  	pluginDir := filepath.Join(".terraform", "providers", "registry.terraform.io", "hashicorp", "null", providerVersion, getproviders.CurrentPlatform.String())
    45  	pluginExe := filepath.Join(pluginDir, "terraform-provider-null_v"+providerVersion+"_x5")
    46  	if getproviders.CurrentPlatform.OS == "windows" {
    47  		pluginExe += ".exe" // ugh
    48  	}
    49  
    50  	// filepath.Join here to make sure we get the right path separator
    51  	// for whatever OS we're running these tests on.
    52  	providerCacheDir := filepath.Join(".terraform", "providers")
    53  
    54  	t.Run("cache dir totally gone", func(t *testing.T) {
    55  		tf := e2e.NewBinary(terraformBin, seedDir)
    56  		defer tf.Close()
    57  		workDir := tf.WorkDir()
    58  
    59  		err := os.RemoveAll(filepath.Join(workDir, ".terraform"))
    60  		if err != nil {
    61  			t.Fatal(err)
    62  		}
    63  
    64  		stdout, stderr, err := tf.Run("plan")
    65  		if err == nil {
    66  			t.Fatalf("unexpected plan success\nstdout:\n%s", stdout)
    67  		}
    68  		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) {
    69  			t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
    70  		}
    71  		if want := `terraform init`; !strings.Contains(stderr, want) {
    72  			t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
    73  		}
    74  
    75  		// Running init as suggested resolves the problem
    76  		_, stderr, err = tf.Run("init")
    77  		if err != nil {
    78  			t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
    79  		}
    80  		_, stderr, err = tf.Run("plan")
    81  		if err != nil {
    82  			t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
    83  		}
    84  	})
    85  	t.Run("cache dir totally gone, explicit backend", func(t *testing.T) {
    86  		tf := e2e.NewBinary(terraformBin, seedDir)
    87  		defer tf.Close()
    88  		workDir := tf.WorkDir()
    89  
    90  		err := ioutil.WriteFile(filepath.Join(workDir, "backend.tf"), []byte(localBackendConfig), 0600)
    91  		if err != nil {
    92  			t.Fatal(err)
    93  		}
    94  
    95  		err = os.RemoveAll(filepath.Join(workDir, ".terraform"))
    96  		if err != nil {
    97  			t.Fatal(err)
    98  		}
    99  
   100  		stdout, stderr, err := tf.Run("plan")
   101  		if err == nil {
   102  			t.Fatalf("unexpected plan success\nstdout:\n%s", stdout)
   103  		}
   104  		if want := `Initial configuration of the requested backend "local"`; !strings.Contains(stderr, want) {
   105  			t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
   106  		}
   107  		if want := `terraform init`; !strings.Contains(stderr, want) {
   108  			t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
   109  		}
   110  
   111  		// Running init as suggested resolves the problem
   112  		_, stderr, err = tf.Run("init")
   113  		if err != nil {
   114  			t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
   115  		}
   116  		_, stderr, err = tf.Run("plan")
   117  		if err != nil {
   118  			t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
   119  		}
   120  	})
   121  	t.Run("null plugin package modified before plan", func(t *testing.T) {
   122  		tf := e2e.NewBinary(terraformBin, seedDir)
   123  		defer tf.Close()
   124  		workDir := tf.WorkDir()
   125  
   126  		err := ioutil.WriteFile(filepath.Join(workDir, pluginExe), []byte("tamper"), 0600)
   127  		if err != nil {
   128  			t.Fatal(err)
   129  		}
   130  
   131  		stdout, stderr, err := tf.Run("plan")
   132  		if err == nil {
   133  			t.Fatalf("unexpected plan success\nstdout:\n%s", stdout)
   134  		}
   135  		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) {
   136  			t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
   137  		}
   138  		if want := `terraform init`; !strings.Contains(stderr, want) {
   139  			t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
   140  		}
   141  	})
   142  	t.Run("version constraint changed in config before plan", func(t *testing.T) {
   143  		tf := e2e.NewBinary(terraformBin, seedDir)
   144  		defer tf.Close()
   145  		workDir := tf.WorkDir()
   146  
   147  		err := ioutil.WriteFile(filepath.Join(workDir, "provider-tampering-base.tf"), []byte(`
   148  			terraform {
   149  				required_providers {
   150  					null = {
   151  						source  = "hashicorp/null"
   152  						version = "1.0.0"
   153  					}
   154  				}
   155  			}
   156  		`), 0600)
   157  		if err != nil {
   158  			t.Fatal(err)
   159  		}
   160  
   161  		stdout, stderr, err := tf.Run("plan")
   162  		if err == nil {
   163  			t.Fatalf("unexpected plan success\nstdout:\n%s", stdout)
   164  		}
   165  		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) {
   166  			t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
   167  		}
   168  		if want := `terraform init -upgrade`; !strings.Contains(stderr, want) {
   169  			t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
   170  		}
   171  	})
   172  	t.Run("lock file modified before plan", func(t *testing.T) {
   173  		tf := e2e.NewBinary(terraformBin, seedDir)
   174  		defer tf.Close()
   175  		workDir := tf.WorkDir()
   176  
   177  		// NOTE: We're just emptying out the lock file here because that's
   178  		// good enough for what we're trying to assert. The leaf codepath
   179  		// that generates this family of errors has some different variations
   180  		// of this error message for otehr sorts of inconsistency, but those
   181  		// are tested more thoroughly over in the "configs" package, which is
   182  		// ultimately responsible for that logic.
   183  		err := ioutil.WriteFile(filepath.Join(workDir, ".terraform.lock.hcl"), []byte(``), 0600)
   184  		if err != nil {
   185  			t.Fatal(err)
   186  		}
   187  
   188  		stdout, stderr, err := tf.Run("plan")
   189  		if err == nil {
   190  			t.Fatalf("unexpected plan success\nstdout:\n%s", stdout)
   191  		}
   192  		if want := `provider registry.terraform.io/hashicorp/null: required by this configuration but no version is selected`; !strings.Contains(stderr, want) {
   193  			t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
   194  		}
   195  		if want := `terraform init`; !strings.Contains(stderr, want) {
   196  			t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
   197  		}
   198  	})
   199  	t.Run("lock file modified after plan", func(t *testing.T) {
   200  		tf := e2e.NewBinary(terraformBin, seedDir)
   201  		defer tf.Close()
   202  		workDir := tf.WorkDir()
   203  
   204  		_, stderr, err := tf.Run("plan", "-out", "tfplan")
   205  		if err != nil {
   206  			t.Fatalf("unexpected plan failure\nstderr:\n%s", stderr)
   207  		}
   208  
   209  		err = os.Remove(filepath.Join(workDir, ".terraform.lock.hcl"))
   210  		if err != nil {
   211  			t.Fatal(err)
   212  		}
   213  
   214  		stdout, stderr, err := tf.Run("apply", "tfplan")
   215  		if err == nil {
   216  			t.Fatalf("unexpected apply success\nstdout:\n%s", stdout)
   217  		}
   218  		if want := `provider registry.terraform.io/hashicorp/null: required by this configuration but no version is selected`; !strings.Contains(stderr, want) {
   219  			t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
   220  		}
   221  		if want := `Create a new plan from the updated configuration.`; !strings.Contains(stderr, want) {
   222  			t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
   223  		}
   224  	})
   225  	t.Run("plugin cache dir entirely removed after plan", func(t *testing.T) {
   226  		tf := e2e.NewBinary(terraformBin, seedDir)
   227  		defer tf.Close()
   228  		workDir := tf.WorkDir()
   229  
   230  		_, stderr, err := tf.Run("plan", "-out", "tfplan")
   231  		if err != nil {
   232  			t.Fatalf("unexpected plan failure\nstderr:\n%s", stderr)
   233  		}
   234  
   235  		err = os.RemoveAll(filepath.Join(workDir, ".terraform"))
   236  		if err != nil {
   237  			t.Fatal(err)
   238  		}
   239  
   240  		stdout, stderr, err := tf.Run("apply", "tfplan")
   241  		if err == nil {
   242  			t.Fatalf("unexpected apply success\nstdout:\n%s", stdout)
   243  		}
   244  		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) {
   245  			t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
   246  		}
   247  	})
   248  	t.Run("null plugin package modified after plan", func(t *testing.T) {
   249  		tf := e2e.NewBinary(terraformBin, seedDir)
   250  		defer tf.Close()
   251  		workDir := tf.WorkDir()
   252  
   253  		_, stderr, err := tf.Run("plan", "-out", "tfplan")
   254  		if err != nil {
   255  			t.Fatalf("unexpected plan failure\nstderr:\n%s", stderr)
   256  		}
   257  
   258  		err = ioutil.WriteFile(filepath.Join(workDir, pluginExe), []byte("tamper"), 0600)
   259  		if err != nil {
   260  			t.Fatal(err)
   261  		}
   262  
   263  		stdout, stderr, err := tf.Run("apply", "tfplan")
   264  		if err == nil {
   265  			t.Fatalf("unexpected apply success\nstdout:\n%s", stdout)
   266  		}
   267  		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) {
   268  			t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, stderr)
   269  		}
   270  	})
   271  }
   272  
   273  const localBackendConfig = `
   274  terraform {
   275    backend "local" {
   276      path = "terraform.tfstate"
   277    }
   278  }
   279  `