2017-07-08 03:49:41 +02:00
package e2etest
import (
2017-09-02 02:24:46 +02:00
"bytes"
"fmt"
"os"
2017-08-16 15:48:51 +02:00
"path/filepath"
2017-09-02 02:24:46 +02:00
"runtime"
2017-07-08 03:49:41 +02:00
"strings"
"testing"
2017-08-16 15:48:51 +02:00
"github.com/hashicorp/terraform/e2e"
2017-07-08 03:49:41 +02:00
)
func TestInitProviders ( t * testing . T ) {
t . Parallel ( )
// This test reaches out to releases.hashicorp.com to download the
// template provider, so it can only run if network access is allowed.
// We intentionally don't try to stub this here, because there's already
// a stubbed version of this in the "command" package and so the goal here
// is to test the interaction with the real repository.
skipIfCannotAccessNetwork ( t )
2019-06-30 09:38:36 +02:00
fixturePath := filepath . Join ( "testdata" , "template-provider" )
2017-08-16 15:48:51 +02:00
tf := e2e . NewBinary ( terraformBin , fixturePath )
2017-07-08 03:49:41 +02:00
defer tf . Close ( )
stdout , stderr , err := tf . Run ( "init" )
if err != nil {
t . Errorf ( "unexpected error: %s" , err )
}
if stderr != "" {
t . Errorf ( "unexpected stderr output:\n%s" , stderr )
}
if ! strings . Contains ( stdout , "Terraform has been successfully initialized!" ) {
t . Errorf ( "success message is missing from output:\n%s" , stdout )
}
2020-04-03 22:59:42 +02:00
if ! strings . Contains ( stdout , "- Installing hashicorp/template v" ) {
2017-07-08 03:49:41 +02:00
t . Errorf ( "provider download message is missing from output:\n%s" , stdout )
t . Logf ( "(this can happen if you have a copy of the plugin in one of the global plugin search dirs)" )
}
2020-10-03 01:41:56 +02:00
if ! strings . Contains ( stdout , "Terraform has created a lock file" ) {
t . Errorf ( "lock file notification is missing from output:\n%s" , stdout )
2017-07-08 03:49:41 +02:00
}
}
2017-09-02 02:24:46 +02:00
2017-11-03 19:36:31 +01:00
func TestInitProvidersInternal ( t * testing . T ) {
t . Parallel ( )
// This test should _not_ reach out anywhere because the "terraform"
// provider is internal to the core terraform binary.
2019-06-30 09:38:36 +02:00
fixturePath := filepath . Join ( "testdata" , "terraform-provider" )
2017-11-03 19:36:31 +01:00
tf := e2e . NewBinary ( terraformBin , fixturePath )
defer tf . Close ( )
stdout , stderr , err := tf . Run ( "init" )
if err != nil {
t . Errorf ( "unexpected error: %s" , err )
}
if stderr != "" {
t . Errorf ( "unexpected stderr output:\n%s" , stderr )
}
if ! strings . Contains ( stdout , "Terraform has been successfully initialized!" ) {
t . Errorf ( "success message is missing from output:\n%s" , stdout )
}
2020-04-03 22:59:42 +02:00
if strings . Contains ( stdout , "Installing hashicorp/terraform" ) {
2017-11-03 19:36:31 +01:00
// Shouldn't have downloaded anything with this config, because the
// provider is built in.
t . Errorf ( "provider download message appeared in output:\n%s" , stdout )
}
2020-04-02 00:32:07 +02:00
if strings . Contains ( stdout , "Installing terraform.io/builtin/terraform" ) {
// Shouldn't have downloaded anything with this config, because the
// provider is built in.
t . Errorf ( "provider download message appeared in output:\n%s" , stdout )
}
}
func TestInitProvidersVendored ( t * testing . T ) {
t . Parallel ( )
// This test will try to reach out to registry.terraform.io as one of the
// possible installation locations for
2020-04-03 22:59:42 +02:00
// hashicorp/null, where it will find that
2020-04-02 00:32:07 +02:00
// versions do exist but will ultimately select the version that is
// vendored due to the version constraint.
skipIfCannotAccessNetwork ( t )
fixturePath := filepath . Join ( "testdata" , "vendored-provider" )
tf := e2e . NewBinary ( terraformBin , fixturePath )
defer tf . Close ( )
main: Consult local directories as potential mirrors of providers
This restores some of the local search directories we used to include when
searching for provider plugins in Terraform 0.12 and earlier. The
directory structures we are expecting in these are different than before,
so existing directory contents will not be compatible without
restructuring, but we need to retain support for these local directories
so that users can continue to sideload third-party provider plugins until
the explicit, first-class provider mirrors configuration (in CLI config)
is implemented, at which point users will be able to override these to
whatever directories they want.
This also includes some new search directories that are specific to the
operating system where Terraform is running, following the documented
layout conventions of that platform. In particular, this follows the
XDG Base Directory specification on Unix systems, which has been a
somewhat-common request to better support "sideloading" of packages via
standard Linux distribution package managers and other similar mechanisms.
While it isn't strictly necessary to add that now, it seems ideal to do
all of the changes to our search directory layout at once so that our
documentation about this can cleanly distinguish "0.12 and earlier" vs.
"0.13 and later", rather than having to document a complex sequence of
smaller changes.
Because this behavior is a result of the integration of package main with
package command, this behavior is verified using an e2etest rather than
a unit test. That test, TestInitProvidersVendored, is also fixed here to
create a suitable directory structure for the platform where the test is
being run. This fixes TestInitProvidersVendored.
2020-04-03 03:04:39 +02:00
// Our fixture dir has a generic os_arch dir, which we need to customize
// to the actual OS/arch where this test is running in order to get the
// desired result.
fixtMachineDir := tf . Path ( "terraform.d/plugins/registry.terraform.io/hashicorp/null/1.0.0+local/os_arch" )
wantMachineDir := tf . Path ( "terraform.d/plugins/registry.terraform.io/hashicorp/null/1.0.0+local/" , fmt . Sprintf ( "%s_%s" , runtime . GOOS , runtime . GOARCH ) )
err := os . Rename ( fixtMachineDir , wantMachineDir )
if err != nil {
t . Fatalf ( "unexpected error: %s" , err )
}
2020-04-02 00:32:07 +02:00
stdout , stderr , err := tf . Run ( "init" )
if err != nil {
t . Errorf ( "unexpected error: %s" , err )
}
if stderr != "" {
t . Errorf ( "unexpected stderr output:\n%s" , stderr )
}
if ! strings . Contains ( stdout , "Terraform has been successfully initialized!" ) {
t . Errorf ( "success message is missing from output:\n%s" , stdout )
}
2020-04-03 22:59:42 +02:00
if ! strings . Contains ( stdout , "- Installing hashicorp/null v1.0.0+local" ) {
2020-04-02 00:32:07 +02:00
t . Errorf ( "provider download message is missing from output:\n%s" , stdout )
t . Logf ( "(this can happen if you have a copy of the plugin in one of the global plugin search dirs)" )
}
2017-11-03 19:36:31 +01:00
}
2020-04-15 20:48:24 +02:00
func TestInitProvidersLocalOnly ( t * testing . T ) {
t . Parallel ( )
// This test should not reach out to the network if it is behaving as
// intended. If it _does_ try to access an upstream registry and encounter
// an error doing so then that's a legitimate test failure that should be
// fixed. (If it incorrectly reaches out anywhere then it's likely to be
// to the host "example.com", which is the placeholder domain we use in
// the test fixture.)
fixturePath := filepath . Join ( "testdata" , "local-only-provider" )
tf := e2e . NewBinary ( terraformBin , fixturePath )
2020-10-13 16:39:35 +02:00
// If you run this test on a workstation with a plugin-cache directory
// configured, it will leave a bad directory behind and terraform init will
// not work until you remove it.
//
// To avoid this, we will "zero out" any existing cli config file.
tf . AddEnv ( "TF_CLI_CONFIG_FILE=\"\"" )
2020-04-15 20:48:24 +02:00
defer tf . Close ( )
// Our fixture dir has a generic os_arch dir, which we need to customize
// to the actual OS/arch where this test is running in order to get the
// desired result.
fixtMachineDir := tf . Path ( "terraform.d/plugins/example.com/awesomecorp/happycloud/1.2.0/os_arch" )
wantMachineDir := tf . Path ( "terraform.d/plugins/example.com/awesomecorp/happycloud/1.2.0/" , fmt . Sprintf ( "%s_%s" , runtime . GOOS , runtime . GOARCH ) )
err := os . Rename ( fixtMachineDir , wantMachineDir )
if err != nil {
t . Fatalf ( "unexpected error: %s" , err )
}
stdout , stderr , err := tf . Run ( "init" )
if err != nil {
t . Errorf ( "unexpected error: %s" , err )
}
if stderr != "" {
t . Errorf ( "unexpected stderr output:\n%s" , stderr )
}
if ! strings . Contains ( stdout , "Terraform has been successfully initialized!" ) {
t . Errorf ( "success message is missing from output:\n%s" , stdout )
}
if ! strings . Contains ( stdout , "- Installing example.com/awesomecorp/happycloud v1.2.0" ) {
t . Errorf ( "provider download message is missing from output:\n%s" , stdout )
t . Logf ( "(this can happen if you have a conflicting copy of the plugin in one of the global plugin search dirs)" )
}
}
2020-04-23 02:13:36 +02:00
func TestInitProvidersCustomMethod ( t * testing . T ) {
t . Parallel ( )
// This test should not reach out to the network if it is behaving as
// intended. If it _does_ try to access an upstream registry and encounter
// an error doing so then that's a legitimate test failure that should be
// fixed. (If it incorrectly reaches out anywhere then it's likely to be
// to the host "example.com", which is the placeholder domain we use in
// the test fixture.)
for _ , configFile := range [ ] string { "cliconfig.tfrc" , "cliconfig.tfrc.json" } {
t . Run ( configFile , func ( t * testing . T ) {
fixturePath := filepath . Join ( "testdata" , "custom-provider-install-method" )
tf := e2e . NewBinary ( terraformBin , fixturePath )
defer tf . Close ( )
// Our fixture dir has a generic os_arch dir, which we need to customize
// to the actual OS/arch where this test is running in order to get the
// desired result.
fixtMachineDir := tf . Path ( "fs-mirror/example.com/awesomecorp/happycloud/1.2.0/os_arch" )
wantMachineDir := tf . Path ( "fs-mirror/example.com/awesomecorp/happycloud/1.2.0/" , fmt . Sprintf ( "%s_%s" , runtime . GOOS , runtime . GOARCH ) )
err := os . Rename ( fixtMachineDir , wantMachineDir )
if err != nil {
t . Fatalf ( "unexpected error: %s" , err )
}
// We'll use a local CLI configuration file taken from our fixture
// directory so we can force a custom installation method config.
tf . AddEnv ( "TF_CLI_CONFIG_FILE=" + tf . Path ( configFile ) )
stdout , stderr , err := tf . Run ( "init" )
if err != nil {
t . Errorf ( "unexpected error: %s" , err )
}
if stderr != "" {
t . Errorf ( "unexpected stderr output:\n%s" , stderr )
}
if ! strings . Contains ( stdout , "Terraform has been successfully initialized!" ) {
t . Errorf ( "success message is missing from output:\n%s" , stdout )
}
if ! strings . Contains ( stdout , "- Installing example.com/awesomecorp/happycloud v1.2.0" ) {
t . Errorf ( "provider download message is missing from output:\n%s" , stdout )
}
} )
}
}
2017-09-02 02:24:46 +02:00
func TestInitProviders_pluginCache ( t * testing . T ) {
t . Parallel ( )
// This test reaches out to releases.hashicorp.com to access plugin
// metadata, and download the null plugin, though the template plugin
// should come from local cache.
skipIfCannotAccessNetwork ( t )
2019-06-30 09:38:36 +02:00
fixturePath := filepath . Join ( "testdata" , "plugin-cache" )
2017-09-02 02:24:46 +02:00
tf := e2e . NewBinary ( terraformBin , fixturePath )
defer tf . Close ( )
// Our fixture dir has a generic os_arch dir, which we need to customize
// to the actual OS/arch where this test is running in order to get the
// desired result.
2020-04-01 23:59:55 +02:00
fixtMachineDir := tf . Path ( "cache/registry.terraform.io/hashicorp/template/2.1.0/os_arch" )
wantMachineDir := tf . Path ( "cache/registry.terraform.io/hashicorp/template/2.1.0/" , fmt . Sprintf ( "%s_%s" , runtime . GOOS , runtime . GOARCH ) )
err := os . Rename ( fixtMachineDir , wantMachineDir )
if err != nil {
t . Fatalf ( "unexpected error: %s" , err )
}
2017-09-02 02:24:46 +02:00
cmd := tf . Cmd ( "init" )
2020-05-29 15:24:25 +02:00
// convert the slashes if building for windows.
p := filepath . FromSlash ( "./cache" )
cmd . Env = append ( cmd . Env , "TF_PLUGIN_CACHE_DIR=" + p )
2020-04-01 23:59:55 +02:00
err = cmd . Run ( )
2017-09-02 02:24:46 +02:00
if err != nil {
t . Errorf ( "unexpected error: %s" , err )
}
command: new cache directory .terraform/providers for providers
Terraform v0.10 introduced .terraform/plugins as a cache directory for
automatically-installed plugins, Terraform v0.13 later reorganized the
directory structure inside but retained its purpose as a cache.
The local cache used to also serve as a record of specifically which
packages were selected in a particular working directory, with the intent
that a second run of "terraform init" would always select the same
packages again. That meant that in some sense it behaved a bit like a
local filesystem mirror directory, even though that wasn't its intended
purpose.
Due to some unfortunate miscommunications, somewhere a long the line we
published some documentation that _recommended_ using the cache directory
as if it were a filesystem mirror directory when working with Terraform
Cloud. That was really only working as an accident of implementation
details, and Terraform v0.14 is now going to break that because the source
of record for the currently-selected provider versions is now the
public-facing dependency lock file rather than the contents of an existing
local cache directory on disk.
After some consideration of how to move forward here, this commit
implements a compromise that tries to avoid silently doing anything
surprising while still giving useful guidance to folks who were previously
using the unsupported strategy. Specifically:
- The local cache directory will now be .terraform/providers rather than
.terraform/plugins, because .terraform/plugins is effectively "poisoned"
by the incorrect usage that we can't reliably distinguish from prior
version correct usage.
- The .terraform/plugins directory is now the "legacy cache directory". It
is intentionally _not_ now a filesystem mirror directory, because that
would risk incorrectly interpreting providers automatically installed
by Terraform v0.13 as if they were a local mirror, and thus upgrades
and checksum fetches from the origin registry would be blocked.
- Because of the previous two points, someone who _was_ trying to use the
legacy cache directory as a filesystem mirror would see installation
fail for any providers they manually added to the legacy directory.
To avoid leaving that user stumped as to what went wrong, there's a
heuristic for the case where a non-official provider fails installation
and yet we can see it in the legacy cache directory. If that heuristic
matches then we'll produce a warning message hinting to move the
provider under the terraform.d/plugins directory, which is a _correct_
location for "bundled" provider plugins that belong only to a single
configuration (as opposed to being installed globally on a system).
This does unfortunately mean that anyone who was following the
incorrectly-documented pattern will now encounter an error (and the
aforementioned warning hint) after upgrading to Terraform v0.14. This
seems like the safest compromise because Terraform can't automatically
infer the intent of files it finds in .terraform/plugins in order to
decide automatically how best to handle them.
The internals of the .terraform directory are always considered
implementation detail for a particular Terraform version and so switching
to a new directory for the _actual_ cache directory fits within our usual
set of guarantees, though it's definitely non-ideal in isolation but okay
when taken in the broader context of this problem, where the alternative
would be silent misbehavior when upgrading.
2020-10-14 00:03:56 +02:00
path := filepath . FromSlash ( fmt . Sprintf ( ".terraform/providers/registry.terraform.io/hashicorp/template/2.1.0/%s_%s/terraform-provider-template_v2.1.0_x4" , runtime . GOOS , runtime . GOARCH ) )
2017-09-02 02:24:46 +02:00
content , err := tf . ReadFile ( path )
if err != nil {
t . Fatalf ( "failed to read installed plugin from %s: %s" , path , err )
}
if strings . TrimSpace ( string ( content ) ) != "this is not a real plugin" {
t . Errorf ( "template plugin was not installed from local cache" )
}
command: new cache directory .terraform/providers for providers
Terraform v0.10 introduced .terraform/plugins as a cache directory for
automatically-installed plugins, Terraform v0.13 later reorganized the
directory structure inside but retained its purpose as a cache.
The local cache used to also serve as a record of specifically which
packages were selected in a particular working directory, with the intent
that a second run of "terraform init" would always select the same
packages again. That meant that in some sense it behaved a bit like a
local filesystem mirror directory, even though that wasn't its intended
purpose.
Due to some unfortunate miscommunications, somewhere a long the line we
published some documentation that _recommended_ using the cache directory
as if it were a filesystem mirror directory when working with Terraform
Cloud. That was really only working as an accident of implementation
details, and Terraform v0.14 is now going to break that because the source
of record for the currently-selected provider versions is now the
public-facing dependency lock file rather than the contents of an existing
local cache directory on disk.
After some consideration of how to move forward here, this commit
implements a compromise that tries to avoid silently doing anything
surprising while still giving useful guidance to folks who were previously
using the unsupported strategy. Specifically:
- The local cache directory will now be .terraform/providers rather than
.terraform/plugins, because .terraform/plugins is effectively "poisoned"
by the incorrect usage that we can't reliably distinguish from prior
version correct usage.
- The .terraform/plugins directory is now the "legacy cache directory". It
is intentionally _not_ now a filesystem mirror directory, because that
would risk incorrectly interpreting providers automatically installed
by Terraform v0.13 as if they were a local mirror, and thus upgrades
and checksum fetches from the origin registry would be blocked.
- Because of the previous two points, someone who _was_ trying to use the
legacy cache directory as a filesystem mirror would see installation
fail for any providers they manually added to the legacy directory.
To avoid leaving that user stumped as to what went wrong, there's a
heuristic for the case where a non-official provider fails installation
and yet we can see it in the legacy cache directory. If that heuristic
matches then we'll produce a warning message hinting to move the
provider under the terraform.d/plugins directory, which is a _correct_
location for "bundled" provider plugins that belong only to a single
configuration (as opposed to being installed globally on a system).
This does unfortunately mean that anyone who was following the
incorrectly-documented pattern will now encounter an error (and the
aforementioned warning hint) after upgrading to Terraform v0.14. This
seems like the safest compromise because Terraform can't automatically
infer the intent of files it finds in .terraform/plugins in order to
decide automatically how best to handle them.
The internals of the .terraform directory are always considered
implementation detail for a particular Terraform version and so switching
to a new directory for the _actual_ cache directory fits within our usual
set of guarantees, though it's definitely non-ideal in isolation but okay
when taken in the broader context of this problem, where the alternative
would be silent misbehavior when upgrading.
2020-10-14 00:03:56 +02:00
nullLinkPath := filepath . FromSlash ( fmt . Sprintf ( ".terraform/providers/registry.terraform.io/hashicorp/null/2.1.0/%s_%s/terraform-provider-null_v2.1.0_x4" , runtime . GOOS , runtime . GOARCH ) )
2020-05-29 15:24:25 +02:00
if runtime . GOOS == "windows" {
nullLinkPath = nullLinkPath + ".exe"
}
if ! tf . FileExists ( nullLinkPath ) {
t . Errorf ( "null plugin was not installed into %s" , nullLinkPath )
2017-09-02 02:24:46 +02:00
}
2020-05-29 15:24:25 +02:00
nullCachePath := filepath . FromSlash ( fmt . Sprintf ( "cache/registry.terraform.io/hashicorp/null/2.1.0/%s_%s/terraform-provider-null_v2.1.0_x4" , runtime . GOOS , runtime . GOARCH ) )
if runtime . GOOS == "windows" {
nullCachePath = nullCachePath + ".exe"
}
if ! tf . FileExists ( nullCachePath ) {
t . Errorf ( "null plugin is not in cache after install. expected in: %s" , nullCachePath )
2017-09-02 02:24:46 +02:00
}
}
2017-10-30 16:27:27 +01:00
func TestInit_fromModule ( t * testing . T ) {
t . Parallel ( )
// This test reaches out to registry.terraform.io and github.com to lookup
// and fetch a module.
skipIfCannotAccessNetwork ( t )
2019-06-30 09:38:36 +02:00
fixturePath := filepath . Join ( "testdata" , "empty" )
2017-10-30 16:27:27 +01:00
tf := e2e . NewBinary ( terraformBin , fixturePath )
defer tf . Close ( )
cmd := tf . Cmd ( "init" , "-from-module=hashicorp/vault/aws" )
cmd . Stdin = nil
cmd . Stderr = & bytes . Buffer { }
err := cmd . Run ( )
if err != nil {
t . Errorf ( "unexpected error: %s" , err )
}
stderr := cmd . Stderr . ( * bytes . Buffer ) . String ( )
if stderr != "" {
t . Errorf ( "unexpected stderr output:\n%s" , stderr )
}
content , err := tf . ReadFile ( "main.tf" )
if err != nil {
t . Fatalf ( "failed to read main.tf: %s" , err )
}
if ! bytes . Contains ( content , [ ] byte ( "vault" ) ) {
t . Fatalf ( "main.tf doesn't appear to be a vault configuration: \n%s" , content )
}
}
2020-05-20 16:20:13 +02:00
func TestInitProviderNotFound ( t * testing . T ) {
t . Parallel ( )
// This test will reach out to registry.terraform.io as one of the possible
// installation locations for hashicorp/nonexist, which should not exist.
skipIfCannotAccessNetwork ( t )
fixturePath := filepath . Join ( "testdata" , "provider-not-found" )
tf := e2e . NewBinary ( terraformBin , fixturePath )
defer tf . Close ( )
t . Run ( "registry provider not found" , func ( t * testing . T ) {
_ , stderr , err := tf . Run ( "init" )
if err == nil {
t . Fatal ( "expected error, got success" )
}
2020-09-30 02:51:39 +02:00
oneLineStderr := strings . ReplaceAll ( stderr , "\n" , " " )
if ! strings . Contains ( oneLineStderr , "provider registry registry.terraform.io does not have a provider named registry.terraform.io/hashicorp/nonexist" ) {
2020-05-20 16:20:13 +02:00
t . Errorf ( "expected error message is missing from output:\n%s" , stderr )
}
} )
t . Run ( "local provider not found" , func ( t * testing . T ) {
// The -plugin-dir directory must exist for the provider installer to search it.
pluginDir := tf . Path ( "empty" )
if err := os . Mkdir ( pluginDir , os . ModePerm ) ; err != nil {
t . Fatal ( err )
}
_ , stderr , err := tf . Run ( "init" , "-plugin-dir=" + pluginDir )
if err == nil {
t . Fatal ( "expected error, got success" )
}
2020-09-30 10:30:02 +02:00
if ! strings . Contains ( stderr , "provider registry.terraform.io/hashicorp/nonexist was not\nfound in any of the search locations\n\n - " + pluginDir ) {
2020-05-20 16:20:13 +02:00
t . Errorf ( "expected error message is missing from output:\n%s" , stderr )
}
} )
}
2020-06-25 16:49:48 +02:00
func TestInitProviderWarnings ( t * testing . T ) {
t . Parallel ( )
// This test will reach out to registry.terraform.io as one of the possible
// installation locations for hashicorp/nonexist, which should not exist.
skipIfCannotAccessNetwork ( t )
fixturePath := filepath . Join ( "testdata" , "provider-warnings" )
tf := e2e . NewBinary ( terraformBin , fixturePath )
defer tf . Close ( )
2020-11-30 18:28:37 +01:00
stdout , _ , err := tf . Run ( "init" )
2020-06-25 16:49:48 +02:00
if err == nil {
t . Fatal ( "expected error, got success" )
}
2020-11-30 18:28:37 +01:00
if ! strings . Contains ( stdout , "This provider is archived and no longer needed. The terraform_remote_state\ndata source is built into the latest Terraform release." ) {
t . Errorf ( "expected warning message is missing from output:\n%s" , stdout )
2020-06-25 16:49:48 +02:00
}
}