diff --git a/internal/command/init.go b/internal/command/init.go index 34656d974a..91970fa0ae 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -31,6 +31,7 @@ import ( "github.com/hashicorp/terraform/internal/getproviders/providerreqs" "github.com/hashicorp/terraform/internal/providercache" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" tfversion "github.com/hashicorp/terraform/version" ) @@ -764,7 +765,8 @@ func (c *InitCommand) getProvidersFromConfig( safeOpts.providers = []addrs.Provider{config.Module.StateStore.ProviderAddr} safeOpts.safeInitEnabled = safeInit } - evts := c.prepareInstallerEvents(ctx, reqs, diags, inst, safeOpts, view, views.InitializingProviderPluginFromConfigMessage, views.ReusingPreviousVersionInfo) + postInstallAction := false + evts := c.prepareInstallerEvents(ctx, reqs, diags, inst, safeOpts, &postInstallAction, view, views.InitializingProviderPluginFromConfigMessage, views.ReusingPreviousVersionInfo) ctx = evts.OnContext(ctx) mode := providercache.InstallNewProvidersOnly @@ -804,6 +806,36 @@ func (c *InitCommand) getProvidersFromConfig( return true, nil, diags } + if postInstallAction { + + // If we can receive input then we prompt for ok from the user + lock := configLocks.Provider(config.Module.StateStore.ProviderAddr) + + v, err := c.UIInput().Input(context.Background(), &terraform.InputOpts{ + Id: "approve", + Query: fmt.Sprintf("Do you want to use provider %q (%s), version %s, for managing state?", + lock.Provider().Type, + lock.Provider(), + lock.Version(), + ), + Description: fmt.Sprintf(`Check the dependency lockfile's entry for %q. + Only 'yes' will be accepted to confirm.`, lock.Provider()), + }) + if err != nil { + diags = diags.Append(fmt.Errorf("Failed to approve use of state storage provider: %s", err)) + return true, nil, diags + } + if v != "yes" { + diags = diags.Append( + fmt.Errorf("State storage provider %q (%s) was not approved", + lock.Provider().Type, + lock.Provider(), + ), + ) + return true, nil, diags + } + } + return true, configLocks, diags } @@ -882,7 +914,8 @@ func (c *InitCommand) getProvidersFromState(ctx context.Context, state *states.S // where statuses update in-place, but we can't do that as long as we // are shimming our vt100 output to the legacy console API on Windows. var safeOpts *safeInitOptions - evts := c.prepareInstallerEvents(ctx, reqs, diags, inst, safeOpts, view, views.InitializingProviderPluginFromStateMessage, views.ReusingVersionIdentifiedFromConfig) + var postInstallAction bool + evts := c.prepareInstallerEvents(ctx, reqs, diags, inst, safeOpts, &postInstallAction, view, views.InitializingProviderPluginFromStateMessage, views.ReusingVersionIdentifiedFromConfig) ctx = evts.OnContext(ctx) mode := providercache.InstallNewProvidersOnly @@ -910,6 +943,11 @@ func (c *InitCommand) getProvidersFromState(ctx context.Context, state *states.S return true, nil, diags } + if postInstallAction { + // We don't need to monitor installation events related to the state. + panic("unexpected post-install action after installing providers from state") + } + return true, newLocks, diags } @@ -991,7 +1029,7 @@ type safeInitOptions struct { // prepareInstallerEvents returns an instance of *providercache.InstallerEvents. This struct defines callback functions that will be executed // when a specific type of event occurs during provider installation. // The calling code needs to provide a tfdiags.Diagnostics collection, so that provider installation code returns diags to the calling code using closures -func (c *InitCommand) prepareInstallerEvents(ctx context.Context, reqs providerreqs.Requirements, diags tfdiags.Diagnostics, inst *providercache.Installer, safeOpts *safeInitOptions, view views.Init, initMsg views.InitMessageCode, reuseMsg views.InitMessageCode) *providercache.InstallerEvents { +func (c *InitCommand) prepareInstallerEvents(ctx context.Context, reqs providerreqs.Requirements, diags tfdiags.Diagnostics, inst *providercache.Installer, safeOpts *safeInitOptions, postInstallAction *bool, view views.Init, initMsg views.InitMessageCode, reuseMsg views.InitMessageCode) *providercache.InstallerEvents { // Because we're currently just streaming a series of events sequentially // into the terminal, we're showing only a subset of the events to keep // things relatively concise. Later it'd be nice to have a progress UI @@ -1047,12 +1085,16 @@ func (c *InitCommand) prepareInstallerEvents(ctx context.Context, reqs providerr case getproviders.PackageHTTPURL: if safeOpts.safeInitEnabled { // Code elsewhere will enforce safe installation - allow + // + // That code elsewhere is controlled by this boolean + *postInstallAction = true return nil } else { // Unsafe - block - return getproviders.ErrUnsafeStateStorageProviderDownload{ - Provider: provider, - Version: version, + return getproviders.ErrUnsafeProviderDownload{ + Provider: provider, + Version: version, + UsedForStateStorage: true, } } default: @@ -1234,7 +1276,7 @@ func (c *InitCommand) prepareInstallerEvents(ctx context.Context, reqs providerr // but rather just emit a single general message about it at // the end, by checking ctx.Err(). - case getproviders.ErrUnsafeStateStorageProviderDownload: + case getproviders.ErrUnsafeProviderDownload: diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Unsafe init - TODO", diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 7dd68f6771..69fd2fa1d4 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "io" "log" "net/http" "net/http/httptest" @@ -3387,72 +3388,13 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { testCopyDir(t, testFixturePath("init-with-state-store"), td) t.Chdir(td) - handler := func(resp http.ResponseWriter, req *http.Request) { - path := req.URL.EscapedPath() - - if path == "/providers/v1/hashicorp/test/versions" { - b := fmt.Sprintf(`{ - "id": "hashicorp/test", - "versions": [ - { - "version": "1.2.3", - "protocols": [ - "5.0" - ], - "platforms": [ - { - "os": "%s", - "arch": "%s" - } - ] - } - ], - "warnings": null -}`, - getproviders.CurrentPlatform.OS, - getproviders.CurrentPlatform.Arch, - ) - resp.WriteHeader(200) - resp.Write([]byte(b)) - return - } - - if path == fmt.Sprintf("/providers/v1/hashicorp/test/1.2.3/download/%s/%s", getproviders.CurrentPlatform.OS, getproviders.CurrentPlatform.Arch) { - b := fmt.Sprintf(`{ - "protocols":["5.0"], - "os":"%[1]s", - "arch":"%[2]s", - "filename":"terraform-provider-test_1.2.3_%[1]s_%[2]s.zip", - "download_url":"https://%[3]s/terraform-provider-test/1.2.3/terraform-provider-test_1.2.3_%[1]s_%[2]s.zip", - "shasums_url":"https://%[3]s/terraform-provider-test/1.2.3/terraform-provider-test_1.2.3_SHA256SUMS", - "shasums_signature_url":"https://%[3]s/terraform-provider-test/1.2.3/terraform-provider-test_1.2.3_SHA256SUMS.72D7468F.sig", - "shasum":"589472b56be8277558616075fc5480fcd812ba6dc70e8979375fc6d8750f83ef", - "signing_keys":{ - "gpg_public_keys":[ - { - "key_id":"34365D9472D7468F","ascii_armor":"-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\nZF5q4h4I33PSGDdSvGXn9UMY5Isjpg==\n=7pIB\n-----END PGP PUBLIC KEY BLOCK-----","trust_signature":"","source":"HashiCorp","source_url":"https://www.hashicorp.com/security.html" - } - ] - } -}`, - getproviders.CurrentPlatform.OS, - getproviders.CurrentPlatform.Arch, - req.Host, - ) - resp.WriteHeader(200) - resp.Write([]byte(b)) - return - } - - if path == "/terraform-provider-test/1.2.3/terraform-provider-test_1.2.3_SHA256SUMS" { - panic("here") - } - // Unhandled path - resp.WriteHeader(418) // asking the teapot to make coffee! - resp.Write([]byte(`unhandled path in test mock`)) - } - server := httptest.NewTLSServer(http.HandlerFunc(handler)) - source, close := testRegistrySource(t, server) + source, close := newMockProviderSourceViaHTTP( + t, + map[string][]string{ + "hashicorp/test": {"1.2.3"}, + }, + "", // In this test case the provider will not be downloaded so no address is needed + ) defer close() ui := new(cli.MockUi) @@ -3483,13 +3425,143 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { // Check output output := testOutput.All() - expectedOutput := "Error: State storage providers must be downloaded using -safe-init flag" - if !strings.Contains(output, expectedOutput) { - t.Fatalf("expected output to include %q, but got':\n %s", expectedOutput, output) + expectedOutputs := []string{ + "Error: State storage providers must be downloaded using -safe-init flag", + "Terraform wanted to download registry.terraform.io/hashicorp/test 1.2.3 to be used for pluggable state storage. Please provide -safe-init flag for safe install", + } + for _, expectedOutput := range expectedOutputs { + if !strings.Contains(output, expectedOutput) { + t.Fatalf("expected output to include %q, but got':\n %s", expectedOutput, output) + } + } + unexpectedOutputs := []string{ + // If the only provider that's blocked from installing is the state storage provider, we don't + // want the text below to be rendered with zero providers listed below it. + "some providers could not be installed:\n", + } + for _, unexpectedOutput := range unexpectedOutputs { + if !strings.Contains(output, unexpectedOutput) { + t.Fatalf("expected output to include %q, but got':\n %s", unexpectedOutput, output) + } + } + }) + + t.Run("init: can safely use a new provider when -safe-init is present", func(t *testing.T) { + // Create a temporary, uninitialized working directory with configuration including a state store + td := t.TempDir() + testCopyDir(t, testFixturePath("init-with-state-store"), td) + t.Chdir(td) + + server := httptest.NewUnstartedServer(nil) // Get un-started server so we know the port it'll run on. + source, close := newMockProviderSourceViaHTTP( + t, + map[string][]string{ + "hashicorp/test": {"1.2.3"}, + }, + server.Listener.Addr().String(), + ) + defer close() + + // Supply a download location so that the installation completes ok + // while Terraform still believes it's downloading a provider via HTTP. + providerMetadata, err := source.PackageMeta( + context.Background(), + addrs.NewDefaultProvider("test"), + getproviders.MustParseVersion("1.2.3"), + getproviders.CurrentPlatform, + ) + if err != nil { + t.Fatalf("failed to get provider metadata: %s", err) + } + server.Config = &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + providerLocationPath := strings.ReplaceAll( + providerMetadata.Location.String(), + "https://"+server.Listener.Addr().String(), + "", + ) + if r.URL.Path == providerLocationPath { + // This is the URL that the init command will hit to download the provider, so we return a valid provider archive. + f, err := os.Open(providerMetadata.Filename) + if err != nil { + t.Fatalf("failed to open mock source file: %s", err) + } + defer f.Close() + archiveBytes, err := io.ReadAll(f) + if err != nil { + t.Fatalf("failed to read mock source file: %s", err) + } + w.WriteHeader(http.StatusOK) + w.Write(archiveBytes) + return + } else { + t.Fatalf("unexpected URL path: %s", r.URL.Path) + } + })} + server.StartTLS() + defer server.Close() + + mockProvider := mockPluggableStateStorageProvider() + mockProviderAddress := addrs.NewDefaultProvider("test") + + // Allow the test to respond to the pause in provider installation for + // checking the state storage provider. + defer testInputMap(t, map[string]string{ + "approve": "yes", + })() + + ui := new(cli.MockUi) + view, done := testView(t) + meta := Meta{ + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + }, + ProviderSource: source, + } + // We're only able to allow Terraform to download providers from a mock provider registry if + // we satisfy TLS requirements. This value passed via context allows the eventual http client used + // for downloading the provider to have config making it accept the self-signed certs from our test server. + meta.CallerContext = context.WithValue(context.Background(), "testing", true) + c := &InitCommand{ + Meta: meta, + } + + args := []string{ + "-enable-pluggable-state-storage-experiment=true", + "-safe-init", // In this test the provider is downloaded via HTTP so this flag is necessary. + } + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("expected code 0 exit code, got %d, output: \n%s", code, testOutput.All()) + } + + // Check output + output := testOutput.All() + expectedOutputs := []string{ + "Initializing the state store...", + "Terraform created an empty state file for the default workspace", + "Terraform has been successfully initialized!", + } + for _, expected := range expectedOutputs { + if !strings.Contains(output, expected) { + t.Fatalf("expected output to include %q, but got':\n %s", expected, output) + } + } + + // Assert the dependency lock file was created + lockFile := filepath.Join(td, ".terraform.lock.hcl") + _, err = os.Stat(lockFile) + if os.IsNotExist(err) { + t.Fatal("expected dependency lock file to not exist, but it doesn't") } }) - t.Run("init: can safely use a new provider, create backend state, and create the default workspace", func(t *testing.T) { + t.Run("init: initialising a state store creates a backend state file and create the default workspace", func(t *testing.T) { // Create a temporary, uninitialized working directory with configuration including a state store td := t.TempDir() testCopyDir(t, testFixturePath("init-with-state-store"), td) @@ -3529,7 +3601,8 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { args := []string{ "-enable-pluggable-state-storage-experiment=true", - "-safe-init", + // -safe-init isn't needed when providers are supplied from the filesystem + // and use of testingOverrides causes the provider to come from the filesystem. } code := c.Run(args) testOutput := done(t) @@ -4917,6 +4990,46 @@ func newMockProviderSource(t *testing.T, availableProviderVersions map[string][] return getproviders.NewMockSource(packages, nil), close } +// newMockProviderSourceViaHTTP is similar to newMockProviderSource except that the metadata (PackageMeta) for each provider +// reports that the provider is going to be accessed via HTTP +// +// Provider binaries are not available via the mock HTTP provider source. This source is sufficient only to allow Terraform +// to complete the provider installation process while believing it's installing providers over HTTP. +// This method is not sufficient to enable Terraform to use providers with those names. +// +// When using `newMockProviderSourceViaHTTP` to set a value for `(Meta).ProviderSource` in a test, also set up `testOverrides` +// in the same Meta. That way the provider source will allow the download process to complete, and when Terraform attempts to use +// those binaries it will instead use the testOverride providers. +func newMockProviderSourceViaHTTP(t *testing.T, availableProviderVersions map[string][]string, address string) (source *getproviders.MockSource, close func()) { + t.Helper() + var packages []getproviders.PackageMeta + var closes []func() + close = func() { + for _, f := range closes { + f() + } + } + for source, versions := range availableProviderVersions { + addr := addrs.MustParseProviderSourceString(source) + for _, versionStr := range versions { + version, err := getproviders.ParseVersion(versionStr) + if err != nil { + close() + t.Fatalf("failed to parse %q as a version number for %q: %s", versionStr, addr.ForDisplay(), err) + } + meta, close, err := getproviders.FakePackageMetaViaHTTP(addr, version, getproviders.VersionList{getproviders.MustParseVersion("5.0")}, getproviders.CurrentPlatform, address, "") + if err != nil { + close() + t.Fatalf("failed to prepare fake package for %s %s: %s", addr.ForDisplay(), versionStr, err) + } + closes = append(closes, close) + packages = append(packages, meta) + } + } + + return getproviders.NewMockSource(packages, nil), close +} + // installFakeProviderPackages installs a fake package for the given provider // names (interpreted as a "default" provider address) and versions into the // local plugin cache for the given "meta". diff --git a/internal/getproviders/errors.go b/internal/getproviders/errors.go index c90cc91f2f..628ee276eb 100644 --- a/internal/getproviders/errors.go +++ b/internal/getproviders/errors.go @@ -247,18 +247,28 @@ func ErrIsNotExist(err error) bool { } } -// ErrUnsafeStateStorageProviderDownload is an error type used to indicate that... TOTO +// ErrUnsafeProviderDownload is an error type used to indicate that... TOTO // // This is returned when ... TODO -type ErrUnsafeStateStorageProviderDownload struct { +type ErrUnsafeProviderDownload struct { Provider addrs.Provider Version Version + + // UsedForStateStorage helps determine what Error should be + // returned from the Error method. + // As more use cases for this error type are identified + // in future we will replace the use of this boolean. + UsedForStateStorage bool } -func (err ErrUnsafeStateStorageProviderDownload) Error() string { - return fmt.Sprintf( - "Terraform wanted to download %s %s to be used for pluggable state storage. Please provide -safe-init flag for safe install", // TODO improve wording - err.Provider, - err.Version, - ) +func (err ErrUnsafeProviderDownload) Error() string { + if err.UsedForStateStorage { + return fmt.Sprintf( + "Terraform wanted to download %s %s to be used for pluggable state storage. Please provide -safe-init flag for safe install", // TODO improve wording + err.Provider, + err.Version, + ) + } + + panic("unimplemented error message for unknown use case of ErrUnsafeProviderDownload") } diff --git a/internal/getproviders/mock_source.go b/internal/getproviders/mock_source.go index 930cbe313d..9710dff2ae 100644 --- a/internal/getproviders/mock_source.go +++ b/internal/getproviders/mock_source.go @@ -6,7 +6,6 @@ import ( "crypto/sha256" "fmt" "io" - "io/ioutil" "os" "github.com/hashicorp/terraform/internal/addrs" @@ -153,7 +152,7 @@ func FakePackageMeta(provider addrs.Provider, version Version, protocols Version // should call the callback even if this function returns an error, because // some error conditions leave a partially-created file on disk. func FakeInstallablePackageMeta(provider addrs.Provider, version Version, protocols VersionList, target Platform, execFilename string) (PackageMeta, func(), error) { - f, err := ioutil.TempFile("", "terraform-getproviders-fake-package-") + f, err := os.CreateTemp("", "terraform-getproviders-fake-package-") if err != nil { return PackageMeta{}, func() {}, err } @@ -212,6 +211,76 @@ func FakeInstallablePackageMeta(provider addrs.Provider, version Version, protoc return meta, close, nil } +// This is basically the same as FakePackageMeta, except that we'll use a PackageHTTPURL instead of a PackageLocalArchive, to allow testing of the code paths that handle HTTP downloads. +// The caller is still responsible for calling the close callback to clean up the temporary file, even though the file is only used to calculate the checksum and isn't actually installed from directly. +func FakePackageMetaViaHTTP(provider addrs.Provider, version Version, protocols VersionList, target Platform, locationBaseUrl string, execFilename string) (PackageMeta, func(), error) { + f, err := os.CreateTemp("", "terraform-getproviders-fake-package-") + if err != nil { + return PackageMeta{}, func() {}, err + } + + // After this point, all of our return paths should include this as the + // close callback. + close := func() { + f.Close() + os.Remove(f.Name()) + } + + if execFilename == "" { + execFilename = fmt.Sprintf("terraform-provider-%s_%s", provider.Type, version.String()) + if target.OS == "windows" { + // For a little more (technically unnecessary) realism... + execFilename += ".exe" + } + } + + zw := zip.NewWriter(f) + fw, err := zw.Create(execFilename) + if err != nil { + return PackageMeta{}, close, fmt.Errorf("failed to add %s to mock zip file: %s", execFilename, err) + } + fmt.Fprintf(fw, "This is a fake provider package for %s %s, not a real provider.\n", provider, version) + err = zw.Close() + if err != nil { + return PackageMeta{}, close, fmt.Errorf("failed to close the mock zip file: %s", err) + } + + // Compute the SHA256 checksum of the generated file, to allow package + // authentication code to be exercised. + f.Seek(0, io.SeekStart) + h := sha256.New() + io.Copy(h, f) + checksum := [32]byte{} + h.Sum(checksum[:0]) + + meta := PackageMeta{ + Provider: provider, + Version: version, + ProtocolVersions: protocols, + TargetPlatform: target, + + Location: PackageHTTPURL( + fmt.Sprintf( + "https://%[1]s/terraform-provider-%[2]s/%[3]s/terraform-provider-%[2]s_%[3]s_%[4]s.zip", + locationBaseUrl, + provider.Type, + version.String(), + target.String(), + ), + ), + + // This is a fake filename that mimics what a real registry might + // indicate as a good filename for this package, in case some caller + // intends to use it to name a local copy of the temporary file. + // (At the time of writing, no caller actually does that, but who + // knows what the future holds?) + Filename: f.Name(), + + Authentication: NewArchiveChecksumAuthentication(target, checksum), + } + return meta, close, nil +} + func (s *MockSource) ForDisplay(provider addrs.Provider) string { return "mock source" } diff --git a/internal/providercache/installer.go b/internal/providercache/installer.go index 405d7b40a3..3094ad1dba 100644 --- a/internal/providercache/installer.go +++ b/internal/providercache/installer.go @@ -5,6 +5,7 @@ package providercache import ( "context" + "errors" "fmt" "log" "sort" @@ -786,20 +787,44 @@ type InstallerError struct { } func (err InstallerError) Error() string { - addrs := make([]addrs.Provider, 0, len(err.ProviderErrors)) - for addr := range err.ProviderErrors { - addrs = append(addrs, addr) - } - sort.Slice(addrs, func(i, j int) bool { - return addrs[i].LessThan(addrs[j]) - }) var b strings.Builder - b.WriteString("some providers could not be installed:\n") - for _, addr := range addrs { - providerErr := err.ProviderErrors[addr] - fmt.Fprintf(&b, "- %s: %s\n", addr, providerErr) + + // We want to render the "unsafe provider download" errors separately in the + // final error message, so separate those out here. + unsafeDownloadErrs := map[addrs.Provider]getproviders.ErrUnsafeProviderDownload{} + for p, pErr := range err.ProviderErrors { + var x getproviders.ErrUnsafeProviderDownload + if errors.As(pErr, &x) { + unsafeDownloadErrs[p] = pErr.(getproviders.ErrUnsafeProviderDownload) + delete(err.ProviderErrors, p) + } + } + if len(unsafeDownloadErrs) > 0 { + b.WriteString("Error: State storage providers must be downloaded using -safe-init flag:\n") + for _, pErr := range unsafeDownloadErrs { + b.WriteString(pErr.Error()) + } + if len(err.ProviderErrors) > 0 { + b.WriteString("\n") // separate the errors above from subsequent errors + } + } + + // Process remaining errors, if present after the process above. + if len(err.ProviderErrors) == 0 { + addrs := make([]addrs.Provider, 0, len(err.ProviderErrors)) + for addr := range err.ProviderErrors { + addrs = append(addrs, addr) + } + sort.Slice(addrs, func(i, j int) bool { + return addrs[i].LessThan(addrs[j]) + }) + + b.WriteString("some providers could not be installed:\n") + for _, addr := range addrs { + providerErr := err.ProviderErrors[addr] + fmt.Fprintf(&b, "- %s: %s\n", addr, providerErr) + } } - // Render a PSS-specific security error separate to the list above. return strings.TrimSpace(b.String()) } diff --git a/internal/providercache/package_install.go b/internal/providercache/package_install.go index 5aff2b20a9..dd8f0822b6 100644 --- a/internal/providercache/package_install.go +++ b/internal/providercache/package_install.go @@ -5,7 +5,9 @@ package providercache import ( "context" + "crypto/tls" "fmt" + "net/http" "net/url" "os" "path/filepath" @@ -24,14 +26,24 @@ import ( // specific protocol and set of expectations.) var unzip = getter.ZipDecompressor{} +// func getProviderDownloadClient() *http.Client { +// } + func installFromHTTPURL(ctx context.Context, meta getproviders.PackageMeta, targetDir string, allowedHashes []getproviders.Hash) (*getproviders.PackageAuthenticationResult, error) { urlStr := meta.Location.String() // When we're installing from an HTTP URL we expect the URL to refer to // a zip file. We'll fetch that into a temporary file here and then // delegate to installFromLocalArchive below to actually extract it. + client := httpclient.New() + if ctx.Value("testing") == true { + client.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + } + httpGetter := getter.HttpGetter{ - Client: httpclient.New(), + Client: client, Netrc: true, XTerraformGetDisabled: true, DoNotCheckHeadFirst: true,