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 `