WIP - blocker with testing; TLS required tunneling code or context

pss/discovery-block-download-via-download-event-callbacks
Sarah French 3 months ago
parent 17b69a7b2a
commit bebf59b0c6

@ -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",

File diff suppressed because one or more lines are too long

@ -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")
}

@ -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"
}

@ -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())
}

@ -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,

Loading…
Cancel
Save