pss/discovery-block-download-via-download-event-callbacks
Sarah French 3 months ago
parent e2cd45fa4b
commit ce2479e46a

@ -8,6 +8,7 @@ import (
"fmt"
"log"
"reflect"
"slices"
"sort"
"strings"
@ -434,7 +435,6 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config,
),
))
}
},
QueryPackagesWarning: func(provider addrs.Provider, warnings []string) {
displayWarnings := make([]string, len(warnings))
@ -694,7 +694,16 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config,
// The method downloads any missing providers that aren't already downloaded and then returns
// dependency lock data based on the configuration.
// The dependency lock file itself isn't updated here.
func (c *InitCommand) getProvidersFromConfig(ctx context.Context, config *configs.Config, upgrade bool, pluginDirs []string, flagLockfile string, view views.Init) (output bool, resultingLocks *depsfile.Locks, diags tfdiags.Diagnostics) {
func (c *InitCommand) getProvidersFromConfig(
ctx context.Context,
config *configs.Config,
upgrade bool,
pluginDirs arguments.FlagStringSlice, // Not needed for PSS stuff - for installer sources.
flagLockfile string,
backendInit bool,
safeInit bool,
view views.Init,
) (output bool, resultingLocks *depsfile.Locks, diags tfdiags.Diagnostics) {
ctx, span := tracer.Start(ctx, "install providers from config")
defer span.End()
@ -746,7 +755,16 @@ func (c *InitCommand) getProvidersFromConfig(ctx context.Context, config *config
log.Printf("[DEBUG] will search for provider plugins in %s", pluginDirs)
}
evts := c.prepareInstallerEvents(ctx, reqs, diags, inst, view, views.InitializingProviderPluginFromConfigMessage, views.ReusingPreviousVersionInfo)
// TODO:
// if backendInit = false then either we're using a previously installed provider for state storage,
// or the config is being ignored and an implied local backend will be used instead.
// Think about use of backendInit here and edge cases.
safeOpts := &safeInitOptions{}
if config.Module.StateStore != nil {
safeOpts.providers = []addrs.Provider{config.Module.StateStore.ProviderAddr}
safeOpts.safeInitEnabled = safeInit
}
evts := c.prepareInstallerEvents(ctx, reqs, diags, inst, safeOpts, view, views.InitializingProviderPluginFromConfigMessage, views.ReusingPreviousVersionInfo)
ctx = evts.OnContext(ctx)
mode := providercache.InstallNewProvidersOnly
@ -795,7 +813,7 @@ func (c *InitCommand) getProvidersFromConfig(ctx context.Context, config *config
// The calling code is assumed to have already called getProvidersFromConfig, which is used to
// supply the configLocks argument.
// The dependency lock file itself isn't updated here.
func (c *InitCommand) getProvidersFromState(ctx context.Context, state *states.State, configLocks *depsfile.Locks, upgrade bool, pluginDirs []string, flagLockfile string, view views.Init) (output bool, resultingLocks *depsfile.Locks, diags tfdiags.Diagnostics) {
func (c *InitCommand) getProvidersFromState(ctx context.Context, state *states.State, configLocks *depsfile.Locks, upgrade bool, pluginDirs []string, flagLockfile string, safeInit bool, view views.Init) (output bool, resultingLocks *depsfile.Locks, diags tfdiags.Diagnostics) {
ctx, span := tracer.Start(ctx, "install providers from state")
defer span.End()
@ -863,7 +881,8 @@ func (c *InitCommand) getProvidersFromState(ctx context.Context, state *states.S
// things relatively concise. Later it'd be nice to have a progress UI
// 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.
evts := c.prepareInstallerEvents(ctx, reqs, diags, inst, view, views.InitializingProviderPluginFromStateMessage, views.ReusingVersionIdentifiedFromConfig)
var safeOpts *safeInitOptions
evts := c.prepareInstallerEvents(ctx, reqs, diags, inst, safeOpts, view, views.InitializingProviderPluginFromStateMessage, views.ReusingVersionIdentifiedFromConfig)
ctx = evts.OnContext(ctx)
mode := providercache.InstallNewProvidersOnly
@ -898,7 +917,6 @@ func (c *InitCommand) getProvidersFromState(ctx context.Context, state *states.S
// The calling code is expected to provide the previous locks (if any) and the two sets of locks determined from
// configuration and state data.
func (c *InitCommand) saveDependencyLockFile(previousLocks, configLocks, stateLocks *depsfile.Locks, flagLockfile string, view views.Init) (output bool, diags tfdiags.Diagnostics) {
// Get the combination of config and state locks
newLocks := c.mergeLockedDependencies(configLocks, stateLocks)
@ -964,11 +982,16 @@ func (c *InitCommand) saveDependencyLockFile(previousLocks, configLocks, stateLo
return output, diags
}
// safeInitOptions
type safeInitOptions struct {
providers []addrs.Provider
safeInitEnabled bool
}
// 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, 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, 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
@ -1007,6 +1030,37 @@ func (c *InitCommand) prepareInstallerEvents(ctx context.Context, reqs providerr
},
FetchPackageBegin: func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation) {
view.LogInitMessage(views.InstallingProviderMessage, provider.ForDisplay(), version)
view.Log("location = " + location.String())
},
FetchPackageSafetyCheck: func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation) error {
// If there are no safe opts the calling code has decided there's no need to be cautious
// in the current scenario.
if safeOpts != nil {
if slices.Contains(safeOpts.providers, provider) {
switch location.(type) {
case getproviders.PackageLocalDir:
// Ok, as providers sourced from the local directory are considered trusted.
return nil
case getproviders.PackageLocalArchive:
// Ok, as providers sourced from a local archive are considered trusted.
return nil
case getproviders.PackageHTTPURL:
if safeOpts.safeInitEnabled {
// Code elsewhere will enforce safe installation - allow
return nil
} else {
// Unsafe - block
return getproviders.ErrUnsafeStateStorageProviderDownload{
Provider: provider,
Version: version,
}
}
default:
panic(fmt.Sprintf("unexpected install location for provider %s %s: %s", provider, version, location))
}
}
}
return nil
},
QueryPackagesFailure: func(provider addrs.Provider, err error) {
switch errorTy := err.(type) {
@ -1095,7 +1149,6 @@ func (c *InitCommand) prepareInstallerEvents(ctx context.Context, reqs providerr
),
))
}
},
QueryPackagesWarning: func(provider addrs.Provider, warnings []string) {
displayWarnings := make([]string, len(warnings))
@ -1181,6 +1234,16 @@ 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:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unsafe init - TODO",
fmt.Sprintf(
"TODO - blocked download of provider %s v%s.",
err.Provider, err.Version,
),
))
default:
// We can potentially end up in here under cancellation too,
// in spite of our getproviders.ErrRequestCanceled case above,

@ -188,7 +188,7 @@ func (c *InitCommand) runPssInit(initArgs *arguments.Init, view views.Init) int
previousLocks, moreDiags := c.lockedDependencies()
diags = diags.Append(moreDiags)
configProvidersOutput, configLocks, configProviderDiags := c.getProvidersFromConfig(ctx, config, initArgs.Upgrade, initArgs.PluginPath, initArgs.Lockfile, view)
configProvidersOutput, configLocks, configProviderDiags := c.getProvidersFromConfig(ctx, config, initArgs.Upgrade, initArgs.PluginPath, initArgs.Lockfile, initArgs.Backend, initArgs.SafeInitWithPluggableStateStore, view)
diags = diags.Append(configProviderDiags)
if configProviderDiags.HasErrors() {
view.Diagnostics(diags)
@ -257,7 +257,7 @@ func (c *InitCommand) runPssInit(initArgs *arguments.Init, view views.Init) int
// Now the resource state is loaded, we can download the providers specified in the state but not the configuration.
// This is step two of a two-step provider download process
stateProvidersOutput, stateLocks, stateProvidersDiags := c.getProvidersFromState(ctx, state, configLocks, initArgs.Upgrade, initArgs.PluginPath, initArgs.Lockfile, view)
stateProvidersOutput, stateLocks, stateProvidersDiags := c.getProvidersFromState(ctx, state, configLocks, initArgs.Upgrade, initArgs.PluginPath, initArgs.Lockfile, initArgs.SafeInitWithPluggableStateStore, view)
diags = diags.Append(configProviderDiags)
if stateProvidersDiags.HasErrors() {
view.Diagnostics(diags)
@ -522,7 +522,6 @@ However, if you intended to override a defined backend, please verify that
the backend configuration is present and valid.
`,
))
}
opts = &BackendOpts{

@ -3381,7 +3381,7 @@ func TestInit_testsWithModule(t *testing.T) {
// Testing init's behaviors with `state_store` when run in an empty working directory
func TestInit_stateStore_newWorkingDir(t *testing.T) {
t.Run("the init command creates a backend state file, and creates the default workspace by default", func(t *testing.T) {
t.Run("int: return error if -safe-init isn't set when downloading the state storage provider", 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)
@ -3413,7 +3413,66 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) {
Meta: meta,
}
args := []string{"-enable-pluggable-state-storage-experiment=true"}
args := []string{
"-enable-pluggable-state-storage-experiment=true",
// -safe-init is omitted to create the test scenario
}
code := c.Run(args)
testOutput := done(t)
if code != 1 {
t.Fatalf("expected code 1 exit code, got %d, output: \n%s", code, testOutput.All())
}
// 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)
}
})
t.Run("init: can safely use a new provider, create backend state, 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)
t.Chdir(td)
mockProvider := mockPluggableStateStorageProvider()
mockProviderAddress := addrs.NewDefaultProvider("test")
providerSource, close := newMockProviderSource(t, map[string][]string{
// The test fixture config has no version constraints, so the latest version will
// be used; below is the 'latest' version in the test world.
"hashicorp/test": {"1.2.3"},
})
defer close()
// 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: providerSource,
}
c := &InitCommand{
Meta: meta,
}
args := []string{
"-enable-pluggable-state-storage-experiment=true",
"-safe-init",
}
code := c.Run(args)
testOutput := done(t)
if code != 0 {
@ -3433,6 +3492,13 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) {
}
}
// 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")
}
// Assert the default workspace was created
if _, exists := mockProvider.MockStates[backend.DefaultStateName]; !exists {
t.Fatal("expected the default workspace to be created during init, but it is missing")

@ -220,8 +220,7 @@ func (err ErrQueryFailed) Unwrap() error {
// This error type doesn't include information about what was cancelled,
// because the expected treatment of this error type is to quickly abort and
// exit with minimal ceremony.
type ErrRequestCanceled struct {
}
type ErrRequestCanceled struct{}
func (err ErrRequestCanceled) Error() string {
return "request canceled"
@ -247,3 +246,19 @@ func ErrIsNotExist(err error) bool {
return false
}
}
// ErrUnsafeStateStorageProviderDownload is an error type used to indicate that... TOTO
//
// This is returned when ... TODO
type ErrUnsafeStateStorageProviderDownload struct {
Provider addrs.Provider
Version Version
}
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,
)
}

@ -556,6 +556,21 @@ NeedProvider:
// Step 3c: Retrieve the package indicated by the metadata we received,
// either directly into our target directory or via the global cache
// directory.
// Before we start the download process, perform and safety checks defined
// by the calling code.
if cb := evts.FetchPackageSafetyCheck; cb != nil {
err := cb(provider, version, meta.Location)
// A returned error means we're blocking download of a provider.
if err != nil {
errs[provider] = err
if cb := evts.FetchPackageFailure; cb != nil {
cb(provider, version, err)
}
continue
}
}
if cb := evts.FetchPackageBegin; cb != nil {
cb(provider, version, meta.Location)
}
@ -784,5 +799,7 @@ func (err InstallerError) Error() string {
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())
}

@ -104,10 +104,16 @@ type InstallerEvents struct {
//
// The Query, Begin, Success, and Failure events will each occur only once
// per distinct provider.
FetchPackageMeta func(provider addrs.Provider, version getproviders.Version) // fetching metadata prior to real download
FetchPackageBegin func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation)
FetchPackageSuccess func(provider addrs.Provider, version getproviders.Version, localDir string, authResult *getproviders.PackageAuthenticationResult)
FetchPackageFailure func(provider addrs.Provider, version getproviders.Version, err error)
//
// The SafetyCheck event allows calling code to inject custom logic that inspects
// a provider that is about to be downloaded. Currently this is used to manage scenarios
// where a provider binary may be downloaded and immediately executed during the
// same init command.
FetchPackageMeta func(provider addrs.Provider, version getproviders.Version) // fetching metadata prior to real download
FetchPackageBegin func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation)
FetchPackageSuccess func(provider addrs.Provider, version getproviders.Version, localDir string, authResult *getproviders.PackageAuthenticationResult)
FetchPackageFailure func(provider addrs.Provider, version getproviders.Version, err error)
FetchPackageSafetyCheck func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation) error
// The ProvidersLockUpdated event is called whenever the lock file will be
// updated. It provides the following information:

Loading…
Cancel
Save