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 `