github.com/opentofu/opentofu@v1.7.1/internal/command/e2etest/providers_tamper_test.go (about)

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