github.com/opentofu/opentofu@v1.7.1/internal/command/e2etest/init_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 "bytes" 10 "encoding/json" 11 "fmt" 12 "os" 13 "path/filepath" 14 "runtime" 15 "strings" 16 "testing" 17 18 "github.com/google/go-cmp/cmp" 19 20 "github.com/opentofu/opentofu/internal/e2e" 21 ) 22 23 func TestInitProviders(t *testing.T) { 24 t.Parallel() 25 26 // This test reaches out to registry.opentofu.org to download the 27 // template provider, so it can only run if network access is allowed. 28 // We intentionally don't try to stub this here, because there's already 29 // a stubbed version of this in the "command" package and so the goal here 30 // is to test the interaction with the real repository. 31 skipIfCannotAccessNetwork(t) 32 33 fixturePath := filepath.Join("testdata", "template-provider") 34 tf := e2e.NewBinary(t, tofuBin, fixturePath) 35 36 stdout, stderr, err := tf.Run("init") 37 if err != nil { 38 t.Errorf("unexpected error: %s", err) 39 } 40 41 if stderr != "" { 42 t.Errorf("unexpected stderr output:\n%s", stderr) 43 } 44 45 if !strings.Contains(stdout, "OpenTofu has been successfully initialized!") { 46 t.Errorf("success message is missing from output:\n%s", stdout) 47 } 48 49 if !strings.Contains(stdout, "- Installing hashicorp/template v") { 50 t.Errorf("provider download message is missing from output:\n%s", stdout) 51 t.Logf("(this can happen if you have a copy of the plugin in one of the global plugin search dirs)") 52 } 53 54 if !strings.Contains(stdout, "OpenTofu has created a lock file") { 55 t.Errorf("lock file notification is missing from output:\n%s", stdout) 56 } 57 58 } 59 60 func TestInitProvidersInternal(t *testing.T) { 61 t.Parallel() 62 63 // This test should _not_ reach out anywhere because the "terraform" 64 // provider is internal to the core tofu binary. 65 66 t.Run("output in human readable format", func(t *testing.T) { 67 fixturePath := filepath.Join("testdata", "tf-provider") 68 tf := e2e.NewBinary(t, tofuBin, fixturePath) 69 70 stdout, stderr, err := tf.Run("init") 71 if err != nil { 72 t.Errorf("unexpected error: %s", err) 73 } 74 75 if stderr != "" { 76 t.Errorf("unexpected stderr output:\n%s", stderr) 77 } 78 79 if !strings.Contains(stdout, "OpenTofu has been successfully initialized!") { 80 t.Errorf("success message is missing from output:\n%s", stdout) 81 } 82 83 if strings.Contains(stdout, "Installing hashicorp/terraform") { 84 // Shouldn't have downloaded anything with this config, because the 85 // provider is built in. 86 t.Errorf("provider download message appeared in output:\n%s", stdout) 87 } 88 89 if strings.Contains(stdout, "Installing terraform.io/builtin/terraform") { 90 // Shouldn't have downloaded anything with this config, because the 91 // provider is built in. 92 t.Errorf("provider download message appeared in output:\n%s", stdout) 93 } 94 }) 95 96 t.Run("output in machine readable format", func(t *testing.T) { 97 fixturePath := filepath.Join("testdata", "tf-provider") 98 tf := e2e.NewBinary(t, tofuBin, fixturePath) 99 100 stdout, stderr, err := tf.Run("init", "-json") 101 if err != nil { 102 t.Errorf("unexpected error: %s", err) 103 } 104 105 if stderr != "" { 106 t.Errorf("unexpected stderr output:\n%s", stderr) 107 } 108 109 // we can not check timestamp, so the sub string is not a valid json object 110 if !strings.Contains(stdout, `{"@level":"info","@message":"OpenTofu has been successfully initialized!","@module":"tofu.ui"`) { 111 t.Errorf("success message is missing from output:\n%s", stdout) 112 } 113 114 if strings.Contains(stdout, "Installing hashicorp/terraform") { 115 // Shouldn't have downloaded anything with this config, because the 116 // provider is built in. 117 t.Errorf("provider download message appeared in output:\n%s", stdout) 118 } 119 120 if strings.Contains(stdout, "Installing terraform.io/builtin/terraform") { 121 // Shouldn't have downloaded anything with this config, because the 122 // provider is built in. 123 t.Errorf("provider download message appeared in output:\n%s", stdout) 124 } 125 }) 126 127 } 128 129 func TestInitProvidersVendored(t *testing.T) { 130 t.Parallel() 131 132 // This test will try to reach out to registry.opentofu.org as one of the 133 // possible installation locations for 134 // hashicorp/null, where it will find that 135 // versions do exist but will ultimately select the version that is 136 // vendored due to the version constraint. 137 skipIfCannotAccessNetwork(t) 138 139 fixturePath := filepath.Join("testdata", "vendored-provider") 140 tf := e2e.NewBinary(t, tofuBin, fixturePath) 141 142 // Our fixture dir has a generic os_arch dir, which we need to customize 143 // to the actual OS/arch where this test is running in order to get the 144 // desired result. 145 fixtMachineDir := tf.Path("terraform.d/plugins/registry.opentofu.org/hashicorp/null/1.0.0+local/os_arch") 146 wantMachineDir := tf.Path("terraform.d/plugins/registry.opentofu.org/hashicorp/null/1.0.0+local/", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)) 147 err := os.Rename(fixtMachineDir, wantMachineDir) 148 if err != nil { 149 t.Fatalf("unexpected error: %s", err) 150 } 151 152 stdout, stderr, err := tf.Run("init") 153 if err != nil { 154 t.Errorf("unexpected error: %s", err) 155 } 156 157 if stderr != "" { 158 t.Errorf("unexpected stderr output:\n%s", stderr) 159 } 160 161 if !strings.Contains(stdout, "OpenTofu has been successfully initialized!") { 162 t.Errorf("success message is missing from output:\n%s", stdout) 163 } 164 165 if !strings.Contains(stdout, "- Installing hashicorp/null v1.0.0+local") { 166 t.Errorf("provider download message is missing from output:\n%s", stdout) 167 t.Logf("(this can happen if you have a copy of the plugin in one of the global plugin search dirs)") 168 } 169 170 } 171 172 func TestInitProvidersLocalOnly(t *testing.T) { 173 t.Parallel() 174 175 // This test should not reach out to the network if it is behaving as 176 // intended. If it _does_ try to access an upstream registry and encounter 177 // an error doing so then that's a legitimate test failure that should be 178 // fixed. (If it incorrectly reaches out anywhere then it's likely to be 179 // to the host "example.com", which is the placeholder domain we use in 180 // the test fixture.) 181 182 t.Run("output in human readable format", func(t *testing.T) { 183 fixturePath := filepath.Join("testdata", "local-only-provider") 184 tf := e2e.NewBinary(t, tofuBin, fixturePath) 185 // If you run this test on a workstation with a plugin-cache directory 186 // configured, it will leave a bad directory behind and tofu init will 187 // not work until you remove it. 188 // 189 // To avoid this, we will "zero out" any existing cli config file. 190 tf.AddEnv("TF_CLI_CONFIG_FILE=") 191 192 // Our fixture dir has a generic os_arch dir, which we need to customize 193 // to the actual OS/arch where this test is running in order to get the 194 // desired result. 195 fixtMachineDir := tf.Path("terraform.d/plugins/example.com/awesomecorp/happycloud/1.2.0/os_arch") 196 wantMachineDir := tf.Path("terraform.d/plugins/example.com/awesomecorp/happycloud/1.2.0/", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)) 197 err := os.Rename(fixtMachineDir, wantMachineDir) 198 if err != nil { 199 t.Fatalf("unexpected error: %s", err) 200 } 201 202 stdout, stderr, err := tf.Run("init") 203 if err != nil { 204 t.Errorf("unexpected error: %s", err) 205 } 206 207 if stderr != "" { 208 t.Errorf("unexpected stderr output:\n%s", stderr) 209 } 210 211 if !strings.Contains(stdout, "OpenTofu has been successfully initialized!") { 212 t.Errorf("success message is missing from output:\n%s", stdout) 213 } 214 215 if !strings.Contains(stdout, "- Installing example.com/awesomecorp/happycloud v1.2.0") { 216 t.Errorf("provider download message is missing from output:\n%s", stdout) 217 t.Logf("(this can happen if you have a conflicting copy of the plugin in one of the global plugin search dirs)") 218 } 219 }) 220 221 t.Run("output in machine readable format", func(t *testing.T) { 222 fixturePath := filepath.Join("testdata", "local-only-provider") 223 tf := e2e.NewBinary(t, tofuBin, fixturePath) 224 // If you run this test on a workstation with a plugin-cache directory 225 // configured, it will leave a bad directory behind and tofu init will 226 // not work until you remove it. 227 // 228 // To avoid this, we will "zero out" any existing cli config file. 229 tf.AddEnv("TF_CLI_CONFIG_FILE=") 230 231 // Our fixture dir has a generic os_arch dir, which we need to customize 232 // to the actual OS/arch where this test is running in order to get the 233 // desired result. 234 fixtMachineDir := tf.Path("terraform.d/plugins/example.com/awesomecorp/happycloud/1.2.0/os_arch") 235 wantMachineDir := tf.Path("terraform.d/plugins/example.com/awesomecorp/happycloud/1.2.0/", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)) 236 err := os.Rename(fixtMachineDir, wantMachineDir) 237 if err != nil { 238 t.Fatalf("unexpected error: %s", err) 239 } 240 241 stdout, stderr, err := tf.Run("init", "-json") 242 if err != nil { 243 t.Errorf("unexpected error: %s", err) 244 } 245 246 if stderr != "" { 247 t.Errorf("unexpected stderr output:\n%s", stderr) 248 } 249 250 // we can not check timestamp, so the sub string is not a valid json object 251 if !strings.Contains(stdout, `{"@level":"info","@message":"OpenTofu has been successfully initialized!","@module":"tofu.ui"`) { 252 t.Errorf("success message is missing from output:\n%s", stdout) 253 } 254 255 if !strings.Contains(stdout, `{"@level":"info","@message":"- Installing example.com/awesomecorp/happycloud v1.2.0...","@module":"tofu.ui"`) { 256 t.Errorf("provider download message is missing from output:\n%s", stdout) 257 t.Logf("(this can happen if you have a conflicting copy of the plugin in one of the global plugin search dirs)") 258 } 259 }) 260 261 } 262 263 func TestInitProvidersCustomMethod(t *testing.T) { 264 t.Parallel() 265 266 // This test should not reach out to the network if it is behaving as 267 // intended. If it _does_ try to access an upstream registry and encounter 268 // an error doing so then that's a legitimate test failure that should be 269 // fixed. (If it incorrectly reaches out anywhere then it's likely to be 270 // to the host "example.com", which is the placeholder domain we use in 271 // the test fixture.) 272 273 for _, configFile := range []string{"cliconfig.tfrc", "cliconfig.tfrc.json"} { 274 t.Run(configFile, func(t *testing.T) { 275 fixturePath := filepath.Join("testdata", "custom-provider-install-method") 276 tf := e2e.NewBinary(t, tofuBin, fixturePath) 277 278 // Our fixture dir has a generic os_arch dir, which we need to customize 279 // to the actual OS/arch where this test is running in order to get the 280 // desired result. 281 fixtMachineDir := tf.Path("fs-mirror/example.com/awesomecorp/happycloud/1.2.0/os_arch") 282 wantMachineDir := tf.Path("fs-mirror/example.com/awesomecorp/happycloud/1.2.0/", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)) 283 err := os.Rename(fixtMachineDir, wantMachineDir) 284 if err != nil { 285 t.Fatalf("unexpected error: %s", err) 286 } 287 288 // We'll use a local CLI configuration file taken from our fixture 289 // directory so we can force a custom installation method config. 290 tf.AddEnv("TF_CLI_CONFIG_FILE=" + tf.Path(configFile)) 291 292 stdout, stderr, err := tf.Run("init") 293 if err != nil { 294 t.Errorf("unexpected error: %s", err) 295 } 296 297 if stderr != "" { 298 t.Errorf("unexpected stderr output:\n%s", stderr) 299 } 300 301 if !strings.Contains(stdout, "OpenTofu has been successfully initialized!") { 302 t.Errorf("success message is missing from output:\n%s", stdout) 303 } 304 305 if !strings.Contains(stdout, "- Installing example.com/awesomecorp/happycloud v1.2.0") { 306 t.Errorf("provider download message is missing from output:\n%s", stdout) 307 } 308 }) 309 } 310 } 311 312 func TestInitProviders_pluginCache(t *testing.T) { 313 t.Parallel() 314 315 // This test reaches out to registry.opentofu.org to access plugin 316 // metadata, and download the null plugin, though the template plugin 317 // should come from local cache. 318 skipIfCannotAccessNetwork(t) 319 320 fixturePath := filepath.Join("testdata", "plugin-cache") 321 tf := e2e.NewBinary(t, tofuBin, fixturePath) 322 323 // Our fixture dir has a generic os_arch dir, which we need to customize 324 // to the actual OS/arch where this test is running in order to get the 325 // desired result. 326 fixtMachineDir := tf.Path("cache/registry.opentofu.org/hashicorp/template/2.1.0/os_arch") 327 wantMachineDir := tf.Path("cache/registry.opentofu.org/hashicorp/template/2.1.0/", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)) 328 err := os.Rename(fixtMachineDir, wantMachineDir) 329 if err != nil { 330 t.Fatalf("unexpected error: %s", err) 331 } 332 333 cmd := tf.Cmd("init") 334 335 // convert the slashes if building for windows. 336 p := filepath.FromSlash("./cache") 337 cmd.Env = append(cmd.Env, "TF_PLUGIN_CACHE_DIR="+p) 338 err = cmd.Run() 339 if err != nil { 340 t.Errorf("unexpected error: %s", err) 341 } 342 343 path := filepath.FromSlash(fmt.Sprintf(".terraform/providers/registry.opentofu.org/hashicorp/template/2.1.0/%s_%s/terraform-provider-template_v2.1.0_x4", runtime.GOOS, runtime.GOARCH)) 344 content, err := tf.ReadFile(path) 345 if err != nil { 346 t.Fatalf("failed to read installed plugin from %s: %s", path, err) 347 } 348 if strings.TrimSpace(string(content)) != "this is not a real plugin" { 349 t.Errorf("template plugin was not installed from local cache") 350 } 351 352 nullLinkPath := filepath.FromSlash(fmt.Sprintf(".terraform/providers/registry.opentofu.org/hashicorp/null/2.1.0/%s_%s/terraform-provider-null", runtime.GOOS, runtime.GOARCH)) 353 if runtime.GOOS == "windows" { 354 nullLinkPath = nullLinkPath + ".exe" 355 } 356 if !tf.FileExists(nullLinkPath) { 357 t.Errorf("null plugin was not installed into %s", nullLinkPath) 358 } 359 360 nullCachePath := filepath.FromSlash(fmt.Sprintf("cache/registry.opentofu.org/hashicorp/null/2.1.0/%s_%s/terraform-provider-null", runtime.GOOS, runtime.GOARCH)) 361 if runtime.GOOS == "windows" { 362 nullCachePath = nullCachePath + ".exe" 363 } 364 if !tf.FileExists(nullCachePath) { 365 t.Errorf("null plugin is not in cache after install. expected in: %s", nullCachePath) 366 } 367 } 368 369 func TestInit_fromModule(t *testing.T) { 370 t.Parallel() 371 372 // This test reaches out to registry.opentofu.org and github.com to lookup 373 // and fetch a module. 374 skipIfCannotAccessNetwork(t) 375 376 fixturePath := filepath.Join("testdata", "empty") 377 tf := e2e.NewBinary(t, tofuBin, fixturePath) 378 379 cmd := tf.Cmd("init", "-from-module=hashicorp/vault/aws") 380 cmd.Stdin = nil 381 cmd.Stderr = &bytes.Buffer{} 382 383 err := cmd.Run() 384 if err != nil { 385 t.Errorf("unexpected error: %s", err) 386 } 387 388 stderr := cmd.Stderr.(*bytes.Buffer).String() 389 if stderr != "" { 390 t.Errorf("unexpected stderr output:\n%s", stderr) 391 } 392 393 content, err := tf.ReadFile("main.tf") 394 if err != nil { 395 t.Fatalf("failed to read main.tf: %s", err) 396 } 397 if !bytes.Contains(content, []byte("vault")) { 398 t.Fatalf("main.tf doesn't appear to be a vault configuration: \n%s", content) 399 } 400 } 401 402 func TestInitProviderNotFound(t *testing.T) { 403 t.Parallel() 404 405 // This test will reach out to registry.opentofu.org as one of the possible 406 // installation locations for hashicorp/nonexist, which should not exist. 407 skipIfCannotAccessNetwork(t) 408 409 fixturePath := filepath.Join("testdata", "provider-not-found") 410 tf := e2e.NewBinary(t, tofuBin, fixturePath) 411 412 t.Run("registry provider not found", func(t *testing.T) { 413 _, stderr, err := tf.Run("init", "-no-color") 414 if err == nil { 415 t.Fatal("expected error, got success") 416 } 417 418 oneLineStderr := strings.ReplaceAll(stderr, "\n", " ") 419 if !strings.Contains(oneLineStderr, "provider registry registry.opentofu.org does not have a provider named registry.opentofu.org/hashicorp/nonexist") { 420 t.Errorf("expected error message is missing from output:\n%s", stderr) 421 } 422 423 if !strings.Contains(oneLineStderr, "All modules should specify their required_providers") { 424 t.Errorf("expected error message is missing from output:\n%s", stderr) 425 } 426 }) 427 428 t.Run("registry provider not found output in json format", func(t *testing.T) { 429 stdout, _, err := tf.Run("init", "-no-color", "-json") 430 if err == nil { 431 t.Fatal("expected error, got success") 432 } 433 434 oneLineStdout := strings.ReplaceAll(stdout, "\n", " ") 435 if !strings.Contains(oneLineStdout, `"diagnostic":{"severity":"error","summary":"Failed to query available provider packages","detail":"Could not retrieve the list of available versions for provider hashicorp/nonexist: provider registry registry.opentofu.org does not have a provider named registry.opentofu.org/hashicorp/nonexist\n\nAll modules should specify their required_providers so that external consumers will get the correct providers when using a module. To see which modules are currently depending on hashicorp/nonexist, run the following command:\n tofu providers\n\nIf you believe this provider is missing from the registry, please submit a issue on the OpenTofu Registry https://github.com/opentofu/registry/issues/"},"type":"diagnostic"}`) { 436 t.Errorf("expected error message is missing from output:\n%s", stdout) 437 } 438 }) 439 440 t.Run("local provider not found", func(t *testing.T) { 441 // The -plugin-dir directory must exist for the provider installer to search it. 442 pluginDir := tf.Path("empty-for-json") 443 if err := os.Mkdir(pluginDir, os.ModePerm); err != nil { 444 t.Fatal(err) 445 } 446 447 _, stderr, err := tf.Run("init", "-no-color", "-plugin-dir="+pluginDir) 448 if err == nil { 449 t.Fatal("expected error, got success") 450 } 451 452 if !strings.Contains(stderr, "provider registry.opentofu.org/hashicorp/nonexist was not\nfound in any of the search locations\n\n - "+pluginDir) { 453 t.Errorf("expected error message is missing from output:\n%s", stderr) 454 } 455 }) 456 457 t.Run("local provider not found output in json format", func(t *testing.T) { 458 // The -plugin-dir directory must exist for the provider installer to search it. 459 pluginDir := tf.Path("empty") 460 if err := os.Mkdir(pluginDir, os.ModePerm); err != nil { 461 t.Fatal(err) 462 } 463 464 stdout, _, err := tf.Run("init", "-no-color", "-plugin-dir="+pluginDir, "-json") 465 if err == nil { 466 t.Fatal("expected error, got success") 467 } 468 469 escapedPluginDir := escapeStringJSON(pluginDir) 470 471 if !strings.Contains(stdout, `"diagnostic":{"severity":"error","summary":"Failed to query available provider packages","detail":"Could not retrieve the list of available versions for provider hashicorp/nonexist: provider registry.opentofu.org/hashicorp/nonexist was not found in any of the search locations\n\n - `+escapedPluginDir+`"},"type":"diagnostic"}`) { 472 t.Errorf("expected error message is missing from output (pluginDir = '%s'):\n%s", escapedPluginDir, stdout) 473 } 474 }) 475 476 t.Run("special characters enabled", func(t *testing.T) { 477 _, stderr, err := tf.Run("init") 478 if err == nil { 479 t.Fatal("expected error, got success") 480 } 481 482 expectedErr := `╷ 483 │ Error: Failed to query available provider packages 484 │` + ` ` + ` 485 │ Could not retrieve the list of available versions for provider 486 │ hashicorp/nonexist: provider registry registry.opentofu.org does not have a 487 │ provider named registry.opentofu.org/hashicorp/nonexist 488 │ 489 │ All modules should specify their required_providers so that external 490 │ consumers will get the correct providers when using a module. To see which 491 │ modules are currently depending on hashicorp/nonexist, run the following 492 │ command: 493 │ tofu providers 494 │ 495 │ If you believe this provider is missing from the registry, please submit a 496 │ issue on the OpenTofu Registry https://github.com/opentofu/registry/issues/ 497 ╵ 498 499 ` 500 if stripAnsi(stderr) != expectedErr { 501 t.Errorf("wrong output:\n%s", cmp.Diff(stripAnsi(stderr), expectedErr)) 502 } 503 }) 504 } 505 506 // The following test is temporarily removed until the OpenTofu registry returns a deprecation warning 507 // https://github.com/opentofu/registry/issues/108 508 //func TestInitProviderWarnings(t *testing.T) { 509 // t.Parallel() 510 // 511 // // This test will reach out to registry.terraform.io as one of the possible 512 // // installation locations for hashicorp/terraform, which is an archived package that is no longer needed. 513 // skipIfCannotAccessNetwork(t) 514 // 515 // fixturePath := filepath.Join("testdata", "provider-warnings") 516 // tf := e2e.NewBinary(t, tofuBin, fixturePath) 517 // 518 // stdout, _, err := tf.Run("init") 519 // if err == nil { 520 // t.Fatal("expected error, got success") 521 // } 522 // 523 // if !strings.Contains(stdout, "This provider is archived and no longer needed.") { 524 // t.Errorf("expected warning message is missing from output:\n%s", stdout) 525 // } 526 // 527 //} 528 529 func escapeStringJSON(v string) string { 530 b := &strings.Builder{} 531 532 enc := json.NewEncoder(b) 533 534 enc.SetEscapeHTML(false) 535 536 if err := enc.Encode(v); err != nil { 537 panic("failed to escapeStringJSON: " + v) 538 } 539 540 marshaledV := b.String() 541 542 // shouldn't happen 543 if len(marshaledV) < 2 { 544 return string(marshaledV) 545 } 546 547 return string(marshaledV[1 : len(marshaledV)-2]) 548 }