PSS: Add interactive confirmation of state storage provider trust when initialising a state store for the first time (#38395)

* refactor: Replace use of prepareInstallerEvents method. This will allow finer control of callbacks when implementing security related features

* feat: Users are prompted to approve a provider used for PSS on first use, and only if downloaded via HTTP.

Prompts include signer details and key ID data.

* test: Users see "Authentication: unauthenticated" in prompt if network mirror doesn't include hashes

They'll see authentication data in all other prompt scenarios. There's no auth when using an fs mirror, but when those are in use we trust the providers already and no prompts are raised.

* refactor: Simplify how we prepare installation event callbacks by defining reused callbacks

* refactor: Remove unused parameters from `getProvidersFromState`
pull/38561/head^2
Sarah French 3 days ago committed by GitHub
parent a941456484
commit 7a960db553
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -360,11 +360,23 @@ the backend configuration is present and valid.
return diags
}
// SafeInitAction describes the action that should be taken by Terraform based on whether
// pluggable state storage is in use, if the provider is going to be downloaded via HTTP or not,
// and whether Terraform is being run in automation or not.
type SafeInitAction rune
const (
SafeInitActionInvalid SafeInitAction = 0
SafeInitActionProceed SafeInitAction = 'P'
SafeInitActionPromptForInput SafeInitAction = 'I'
SafeInitActionNotRelevant SafeInitAction = 'N' // For when a state store isn't in use at all!
)
// getProvidersFromConfig determines what providers are required by the given configuration data.
// 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 []string, flagLockfile string, view views.Init) (output bool, resultingLocks *depsfile.Locks, safeInitAction SafeInitAction, authResult *getproviders.PackageAuthenticationResult, diags tfdiags.Diagnostics) {
ctx, span := tracer.Start(ctx, "install providers from config")
defer span.End()
@ -380,7 +392,7 @@ func (c *InitCommand) getProvidersFromConfig(ctx context.Context, config *config
reqs, hclDiags := config.ProviderRequirements()
diags = diags.Append(hclDiags)
if hclDiags.HasErrors() {
return false, nil, diags
return false, nil, SafeInitActionInvalid, nil, diags
}
reqs = c.removeDevOverrides(reqs)
@ -398,7 +410,7 @@ func (c *InitCommand) getProvidersFromConfig(ctx context.Context, config *config
}
}
if diags.HasErrors() {
return false, nil, diags
return false, nil, SafeInitActionInvalid, nil, diags
}
var inst *providercache.Installer
@ -420,7 +432,68 @@ 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)
// Prepare callback functions for the installer.
// These allow us to send output to the terminal as events happen, catch
// diagnostics, etc.
//
// We use some callbacks to capture data that's surfaced during the
// installation process:
// - provider authentication info.
// - info about what type of location a provider is sourced from.
// These pieces of data are used to determine if additional security features
// need to be enabled.
providerLocations := make(map[addrs.Provider]getproviders.PackageLocation)
var stateStoreProviderAuthResult *getproviders.PackageAuthenticationResult
evts := &providercache.InstallerEvents{
PendingProviders: func(reqs map[addrs.Provider]getproviders.VersionConstraints) {
view.Output(views.InitializingProviderPluginFromConfigMessage) // Message is specific to provider download from config
},
ProviderAlreadyInstalled: providerAlreadyInstalledCallback(view),
BuiltInProviderAvailable: builtInProviderAvailableCallback(view),
BuiltInProviderFailure: builtInProviderFailureCallback(&diags),
QueryPackagesBegin: func(provider addrs.Provider, versionConstraints getproviders.VersionConstraints, locked bool) {
if locked {
view.LogInitMessage(views.ReusingPreviousVersionInfo, provider.ForDisplay()) // Message is specific to provide download from config
} else {
if len(versionConstraints) > 0 {
view.LogInitMessage(views.FindingMatchingVersionMessage, provider.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints))
} else {
view.LogInitMessage(views.FindingLatestVersionMessage, provider.ForDisplay())
}
}
},
LinkFromCacheBegin: linkFromCacheBeginCallback(view),
FetchPackageBegin: func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation) {
// 1) Record the location of this provider.
//
// FetchPackageBegin is the callback hook at the start of the process of obtaining a provider that isn't yet
// in the dependency lock file. Providers that are processed here will not be processed here on the next init,
// as then they will be in the lock file. The same provider type would only be processed here again if the
// provider version changed via an `init -upgrade` command.
providerLocations[provider] = location
// 2) Call the shared callback for FetchPackageBegin.
cb := fetchPackageBeginCallback(view)
cb(provider, version, location)
},
QueryPackagesFailure: queryPackagesFailureCallback(&diags, ctx, inst.ProviderSource(), reqs),
QueryPackagesWarning: queryPackagesWarningCallback(&diags),
LinkFromCacheFailure: linkFromCacheFailureCallback(&diags),
FetchPackageFailure: fetchPackageFailureCallback(&diags, reqs),
FetchPackageSuccess: func(provider addrs.Provider, version getproviders.Version, localDir string, authResult *getproviders.PackageAuthenticationResult) {
// 1. Capture auth result if this provider is used for state storage.
if config.Module.StateStore != nil && provider.Equals(config.Module.StateStore.ProviderAddr) {
log.Printf("[TRACE] getProvidersFromConfig: state storage provider %s (%q) auth result: %q", config.Module.StateStore.ProviderAddr.Type, config.Module.StateStore.ProviderAddr.ForDisplay(), stateStoreProviderAuthResult.String())
stateStoreProviderAuthResult = authResult
}
// 2. Call the shared callback for FetchPackageSuccess
cb := fetchPackageSuccessCallback(view)
cb(provider, version, localDir, authResult)
},
ProvidersLockUpdated: providersLockUpdatedCallback(&c.incompleteProviders),
ProvidersFetched: providersFetchedCallback(view),
}
ctx = evts.OnContext(ctx)
mode := providercache.InstallNewProvidersOnly
@ -428,7 +501,7 @@ func (c *InitCommand) getProvidersFromConfig(ctx context.Context, config *config
if flagLockfile == "readonly" {
diags = diags.Append(fmt.Errorf("The -upgrade flag conflicts with -lockfile=readonly."))
view.Diagnostics(diags)
return true, nil, diags
return true, nil, SafeInitActionInvalid, nil, diags
}
mode = providercache.InstallUpgrades
@ -438,7 +511,7 @@ func (c *InitCommand) getProvidersFromConfig(ctx context.Context, config *config
previousLocks, moreDiags := c.lockedDependencies()
diags = diags.Append(moreDiags)
if diags.HasErrors() {
return false, nil, diags
return false, nil, SafeInitActionInvalid, nil, diags
}
// Determine which required providers are already downloaded, and download any
@ -447,7 +520,7 @@ func (c *InitCommand) getProvidersFromConfig(ctx context.Context, config *config
if ctx.Err() == context.Canceled {
diags = diags.Append(fmt.Errorf("Provider installation was canceled by an interrupt signal."))
view.Diagnostics(diags)
return true, nil, diags
return true, nil, SafeInitActionInvalid, nil, diags
}
if err != nil {
// The errors captured in "err" should be redundant with what we
@ -457,10 +530,43 @@ func (c *InitCommand) getProvidersFromConfig(ctx context.Context, config *config
diags = diags.Append(err)
}
return true, nil, diags
return true, nil, SafeInitActionInvalid, nil, diags
}
return true, configLocks, diags
// Return advice to the calling code about what to do regarding safe init feature related to state storage providers
if config.Module.StateStore == nil {
// If PSS isn't in use then return a value that isn't the zero value but isn't misleading.
safeInitAction = SafeInitActionNotRelevant
} else {
location, ok := providerLocations[config.Module.StateStore.ProviderAddr]
if !ok {
// The provider was not processed in the FetchPackageBegin callback.
// A provider that wasn't downloaded during this init could be because:
// * It was already present from a previous installation.
// * If upgrading, no newer version was available that matched version constraints.
// * Or, the provider is unmanaged/reattached and so download was skipped.
log.Printf("[TRACE] init (getProvidersFromConfig): the state storage provider %s (%q) will not be changed in the dependency lock file after provider installation. Either it was already present and/or there was no available upgrade version that matched version constraints.", config.Module.StateStore.ProviderAddr.Type, config.Module.StateStore.ProviderAddr)
safeInitAction = SafeInitActionProceed
} else {
// The provider was processed in the FetchPackageBegin callback, so either it's being downloaded for the first time, or upgraded.
log.Printf("[TRACE] init (getProvidersFromConfig): the state storage provider %s (%q) will be changed in the dependency lock file during provider installation.", config.Module.StateStore.ProviderAddr.Type, config.Module.StateStore.ProviderAddr)
switch location.(type) {
case getproviders.PackageLocalArchive, getproviders.PackageLocalDir:
// If the provider is downloaded from a local source we assume it's safe.
// We don't require presence of the -safe-init flag, or require input from the user to approve its usage.
log.Printf("[TRACE] init (getProvidersFromConfig): the state storage provider %s (%q) is downloaded from a local source, so we consider it safe.", config.Module.StateStore.ProviderAddr.Type, config.Module.StateStore.ProviderAddr)
safeInitAction = SafeInitActionProceed
case getproviders.PackageHTTPURL:
log.Printf("[DEBUG] init (getProvidersFromConfig): the state storage provider %s (%q) is downloaded via HTTP, so we consider it potentially unsafe.", config.Module.StateStore.ProviderAddr.Type, config.Module.StateStore.ProviderAddr)
safeInitAction = SafeInitActionPromptForInput
default:
panic(fmt.Sprintf("init (getProvidersFromConfig): unexpected provider location type for state storage provider %q: %T", config.Module.StateStore.ProviderAddr, location))
}
}
}
return true, configLocks, safeInitAction, stateStoreProviderAuthResult, diags
}
// getProvidersFromState determines what providers are required by the given state data.
@ -469,7 +575,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, configReqs providerreqs.Requirements, 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, configReqs providerreqs.Requirements, configLocks *depsfile.Locks, pluginDirs []string, view views.Init) (output bool, resultingLocks *depsfile.Locks, diags tfdiags.Diagnostics) {
ctx, span := tracer.Start(ctx, "install providers from state")
defer span.End()
@ -545,7 +651,34 @@ 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)
evts := &providercache.InstallerEvents{
PendingProviders: func(reqs map[addrs.Provider]getproviders.VersionConstraints) {
view.Output(views.InitializingProviderPluginFromStateMessage) // Message is specific to provider download from state
},
ProviderAlreadyInstalled: providerAlreadyInstalledCallback(view),
BuiltInProviderAvailable: builtInProviderAvailableCallback(view),
BuiltInProviderFailure: builtInProviderFailureCallback(&diags),
QueryPackagesBegin: func(provider addrs.Provider, versionConstraints getproviders.VersionConstraints, locked bool) {
if locked {
view.LogInitMessage(views.ReusingVersionIdentifiedFromConfig, provider.ForDisplay()) // Message is specific to provider download from state
} else {
if len(versionConstraints) > 0 {
view.LogInitMessage(views.FindingMatchingVersionMessage, provider.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints))
} else {
view.LogInitMessage(views.FindingLatestVersionMessage, provider.ForDisplay())
}
}
},
LinkFromCacheBegin: linkFromCacheBeginCallback(view),
FetchPackageBegin: fetchPackageBeginCallback(view),
QueryPackagesFailure: queryPackagesFailureCallback(&diags, ctx, inst.ProviderSource(), reqs),
QueryPackagesWarning: queryPackagesWarningCallback(&diags),
LinkFromCacheFailure: linkFromCacheFailureCallback(&diags),
FetchPackageFailure: fetchPackageFailureCallback(&diags, reqs),
FetchPackageSuccess: fetchPackageSuccessCallback(view),
ProvidersLockUpdated: providersLockUpdatedCallback(&c.incompleteProviders),
ProvidersFetched: providersFetchedCallback(view),
}
ctx = evts.OnContext(ctx)
mode := providercache.InstallNewProvidersOnly
@ -645,301 +778,6 @@ func (c *InitCommand) saveDependencyLockFile(previousLocks, configLocks, stateLo
return output, diags
}
// 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 {
// 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
// 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.
events := &providercache.InstallerEvents{
PendingProviders: func(reqs map[addrs.Provider]getproviders.VersionConstraints) {
view.Output(initMsg)
},
ProviderAlreadyInstalled: func(provider addrs.Provider, selectedVersion getproviders.Version) {
view.LogInitMessage(views.ProviderAlreadyInstalledMessage, provider.ForDisplay(), selectedVersion)
},
BuiltInProviderAvailable: func(provider addrs.Provider) {
view.LogInitMessage(views.BuiltInProviderAvailableMessage, provider.ForDisplay())
},
BuiltInProviderFailure: func(provider addrs.Provider, err error) {
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid dependency on built-in provider",
fmt.Sprintf("Cannot use %s: %s.", provider.ForDisplay(), err),
))
},
QueryPackagesBegin: func(provider addrs.Provider, versionConstraints getproviders.VersionConstraints, locked bool) {
if locked {
view.LogInitMessage(reuseMsg, provider.ForDisplay())
} else {
if len(versionConstraints) > 0 {
view.LogInitMessage(views.FindingMatchingVersionMessage, provider.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints))
} else {
view.LogInitMessage(views.FindingLatestVersionMessage, provider.ForDisplay())
}
}
},
LinkFromCacheBegin: func(provider addrs.Provider, version getproviders.Version, cacheRoot string) {
view.LogInitMessage(views.UsingProviderFromCacheDirInfo, provider.ForDisplay(), version)
},
FetchPackageBegin: func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation) {
view.LogInitMessage(views.InstallingProviderMessage, provider.ForDisplay(), version)
},
QueryPackagesFailure: func(provider addrs.Provider, err error) {
switch errorTy := err.(type) {
case getproviders.ErrProviderNotFound:
sources := errorTy.Sources
displaySources := make([]string, len(sources))
for i, source := range sources {
displaySources[i] = fmt.Sprintf(" - %s", source)
}
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to query available provider packages",
fmt.Sprintf("Could not retrieve the list of available versions for provider %s: %s\n\n%s",
provider.ForDisplay(), err, strings.Join(displaySources, "\n"),
),
))
case getproviders.ErrRegistryProviderNotKnown:
// We might be able to suggest an alternative provider to use
// instead of this one.
suggestion := fmt.Sprintf("\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 %s, run the following command:\n terraform providers", provider.ForDisplay())
alternative := getproviders.MissingProviderSuggestion(ctx, provider, inst.ProviderSource(), reqs)
if alternative != provider {
suggestion = fmt.Sprintf(
"\n\nDid you intend to use %s? If so, you must specify that source address in each module which requires that provider. To see which modules are currently depending on %s, run the following command:\n terraform providers",
alternative.ForDisplay(), provider.ForDisplay(),
)
}
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to query available provider packages",
fmt.Sprintf("Could not retrieve the list of available versions for provider %s: %s%s",
provider.ForDisplay(), err, suggestion,
),
))
case getproviders.ErrHostNoProviders:
switch {
case errorTy.Hostname == svchost.Hostname("github.com") && !errorTy.HasOtherVersion:
// If a user copies the URL of a GitHub repository into
// the source argument and removes the schema to make it
// provider-address-shaped then that's one way we can end up
// here. We'll use a specialized error message in anticipation
// of that mistake. We only do this if github.com isn't a
// provider registry, to allow for the (admittedly currently
// rather unlikely) possibility that github.com starts being
// a real Terraform provider registry in the future.
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid provider registry host",
fmt.Sprintf("The given source address %q specifies a GitHub repository rather than a Terraform provider. Refer to the documentation of the provider to find the correct source address to use.",
provider.String(),
),
))
case errorTy.HasOtherVersion:
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid provider registry host",
fmt.Sprintf("The host %q given in provider source address %q does not offer a Terraform provider registry that is compatible with this Terraform version, but it may be compatible with a different Terraform version.",
errorTy.Hostname, provider.String(),
),
))
default:
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid provider registry host",
fmt.Sprintf("The host %q given in provider source address %q does not offer a Terraform provider registry.",
errorTy.Hostname, provider.String(),
),
))
}
case getproviders.ErrRequestCanceled:
// We don't attribute cancellation to any particular operation,
// but rather just emit a single general message about it at
// the end, by checking ctx.Err().
default:
suggestion := fmt.Sprintf("\n\nTo see which modules are currently depending on %s and what versions are specified, run the following command:\n terraform providers", provider.ForDisplay())
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to query available provider packages",
fmt.Sprintf("Could not retrieve the list of available versions for provider %s: %s%s",
provider.ForDisplay(), err, suggestion,
),
))
}
},
QueryPackagesWarning: func(provider addrs.Provider, warnings []string) {
displayWarnings := make([]string, len(warnings))
for i, warning := range warnings {
displayWarnings[i] = fmt.Sprintf("- %s", warning)
}
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Additional provider information from registry",
fmt.Sprintf("The remote registry returned warnings for %s:\n%s",
provider.String(),
strings.Join(displayWarnings, "\n"),
),
))
},
LinkFromCacheFailure: func(provider addrs.Provider, version getproviders.Version, err error) {
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to install provider from shared cache",
fmt.Sprintf("Error while importing %s v%s from the shared cache directory: %s.", provider.ForDisplay(), version, err),
))
},
FetchPackageFailure: func(provider addrs.Provider, version getproviders.Version, err error) {
const summaryIncompatible = "Incompatible provider version"
switch err := err.(type) {
case getproviders.ErrProtocolNotSupported:
closestAvailable := err.Suggestion
switch {
case closestAvailable == getproviders.UnspecifiedVersion:
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
summaryIncompatible,
fmt.Sprintf(errProviderVersionIncompatible, provider.String()),
))
case version.GreaterThan(closestAvailable):
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
summaryIncompatible,
fmt.Sprintf(providerProtocolTooNew, provider.ForDisplay(),
version, tfversion.String(), closestAvailable, closestAvailable,
getproviders.VersionConstraintsString(reqs[provider]),
),
))
default: // version is less than closestAvailable
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
summaryIncompatible,
fmt.Sprintf(providerProtocolTooOld, provider.ForDisplay(),
version, tfversion.String(), closestAvailable, closestAvailable,
getproviders.VersionConstraintsString(reqs[provider]),
),
))
}
case getproviders.ErrPlatformNotSupported:
switch {
case err.MirrorURL != nil:
// If we're installing from a mirror then it may just be
// the mirror lacking the package, rather than it being
// unavailable from upstream.
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
summaryIncompatible,
fmt.Sprintf(
"Your chosen provider mirror at %s does not have a %s v%s package available for your current platform, %s.\n\nProvider releases are separate from Terraform CLI releases, so this provider might not support your current platform. Alternatively, the mirror itself might have only a subset of the plugin packages available in the origin registry, at %s.",
err.MirrorURL, err.Provider, err.Version, err.Platform,
err.Provider.Hostname,
),
))
default:
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
summaryIncompatible,
fmt.Sprintf(
"Provider %s v%s does not have a package available for your current platform, %s.\n\nProvider releases are separate from Terraform CLI releases, so not all providers are available for all platforms. Other versions of this provider may have different platforms supported.",
err.Provider, err.Version, err.Platform,
),
))
}
case getproviders.ErrRequestCanceled:
// We don't attribute cancellation to any particular operation,
// but rather just emit a single general message about it at
// the end, by checking ctx.Err().
default:
// We can potentially end up in here under cancellation too,
// in spite of our getproviders.ErrRequestCanceled case above,
// because not all of the outgoing requests we do under the
// "fetch package" banner are source metadata requests.
// In that case we will emit a redundant error here about
// the request being cancelled, but we'll still detect it
// as a cancellation after the installer returns and do the
// normal cancellation handling.
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to install provider",
fmt.Sprintf("Error while installing %s v%s: %s", provider.ForDisplay(), version, err),
))
}
},
FetchPackageSuccess: func(provider addrs.Provider, version getproviders.Version, localDir string, authResult *getproviders.PackageAuthenticationResult) {
var keyID string
if authResult != nil && authResult.ThirdPartySigned() {
keyID = authResult.KeyID
}
if keyID != "" {
keyID = view.PrepareMessage(views.KeyID, keyID)
}
view.LogInitMessage(views.InstalledProviderVersionInfo, provider.ForDisplay(), version, authResult, keyID)
},
ProvidersLockUpdated: func(provider addrs.Provider, version getproviders.Version, localHashes []getproviders.Hash, signedHashes []getproviders.Hash, priorHashes []getproviders.Hash) {
// We're going to use this opportunity to track if we have any
// "incomplete" installs of providers. An incomplete install is
// when we are only going to write the local hashes into our lock
// file which means a `terraform init` command will fail in future
// when used on machines of a different architecture.
//
// We want to print a warning about this.
if len(signedHashes) > 0 {
// If we have any signedHashes hashes then we don't worry - as
// we know we retrieved all available hashes for this version
// anyway.
return
}
// If local hashes and prior hashes are exactly the same then
// it means we didn't record any signed hashes previously, and
// we know we're not adding any extra in now (because we already
// checked the signedHashes), so that's a problem.
//
// In the actual check here, if we have any priorHashes and those
// hashes are not the same as the local hashes then we're going to
// accept that this provider has been configured correctly.
if len(priorHashes) > 0 && !reflect.DeepEqual(localHashes, priorHashes) {
return
}
// Now, either signedHashes is empty, or priorHashes is exactly the
// same as our localHashes which means we never retrieved the
// signedHashes previously.
//
// Either way, this is bad. Let's complain/warn.
c.incompleteProviders = append(c.incompleteProviders, provider.ForDisplay())
},
ProvidersFetched: func(authResults map[addrs.Provider]*getproviders.PackageAuthenticationResult) {
thirdPartySigned := false
for _, authResult := range authResults {
if authResult.ThirdPartySigned() {
thirdPartySigned = true
break
}
}
if thirdPartySigned {
view.LogInitMessage(views.PartnerAndCommunityProvidersMessage)
}
},
}
return events
}
// backendConfigOverrideBody interprets the raw values of -backend-config
// arguments into a hcl Body that should override the backend settings given
// in the configuration.
@ -1175,6 +1013,320 @@ func (c *InitCommand) Synopsis() string {
return "Prepare your working directory for other commands"
}
// Returns a reused callback function for the ProviderAlreadyInstalled event in a providercache.InstallerEvents struct.
func providerAlreadyInstalledCallback(view views.Init) func(provider addrs.Provider, selectedVersion getproviders.Version) {
return func(provider addrs.Provider, selectedVersion getproviders.Version) {
view.LogInitMessage(views.ProviderAlreadyInstalledMessage, provider.ForDisplay(), selectedVersion)
}
}
// Returns a reused callback function for the BuiltInProviderAvailable event in a providercache.InstallerEvents struct.
func builtInProviderAvailableCallback(view views.Init) func(provider addrs.Provider) {
return func(provider addrs.Provider) {
view.LogInitMessage(views.BuiltInProviderAvailableMessage, provider.ForDisplay())
}
}
// Returns a reused callback function for the BuiltinProviderFailure event in a providercache.InstallerEvents struct.
func builtInProviderFailureCallback(diags *tfdiags.Diagnostics) func(provider addrs.Provider, err error) {
return func(provider addrs.Provider, err error) {
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid dependency on built-in provider",
fmt.Sprintf("Cannot use %s: %s.", provider.ForDisplay(), err),
))
}
}
// Returns a reused callback function for the LinkFromCacheBegin event in a providercache.InstallerEvents struct.
func linkFromCacheBeginCallback(view views.Init) func(provider addrs.Provider, version getproviders.Version, cacheRoot string) {
return func(provider addrs.Provider, version getproviders.Version, cacheRoot string) {
view.LogInitMessage(views.UsingProviderFromCacheDirInfo, provider.ForDisplay(), version)
}
}
// Returns a reused callback function for the FetchPackageBegin event in a providercache.InstallerEvents struct.
func fetchPackageBeginCallback(view views.Init) func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation) {
return func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation) {
view.LogInitMessage(views.InstallingProviderMessage, provider.ForDisplay(), version)
}
}
// Returns a reused callback function for the QueryPackagesFailure event in a providercache.InstallerEvents struct.
func queryPackagesFailureCallback(diags *tfdiags.Diagnostics, ctx context.Context, source getproviders.Source, reqs getproviders.Requirements) func(provider addrs.Provider, err error) {
return func(provider addrs.Provider, err error) {
switch errorTy := err.(type) {
case getproviders.ErrProviderNotFound:
sources := errorTy.Sources
displaySources := make([]string, len(sources))
for i, source := range sources {
displaySources[i] = fmt.Sprintf(" - %s", source)
}
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to query available provider packages",
fmt.Sprintf("Could not retrieve the list of available versions for provider %s: %s\n\n%s",
provider.ForDisplay(), err, strings.Join(displaySources, "\n"),
),
))
case getproviders.ErrRegistryProviderNotKnown:
// We might be able to suggest an alternative provider to use
// instead of this one.
suggestion := fmt.Sprintf("\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 %s, run the following command:\n terraform providers", provider.ForDisplay())
alternative := getproviders.MissingProviderSuggestion(ctx, provider, source, reqs)
if alternative != provider {
suggestion = fmt.Sprintf(
"\n\nDid you intend to use %s? If so, you must specify that source address in each module which requires that provider. To see which modules are currently depending on %s, run the following command:\n terraform providers",
alternative.ForDisplay(), provider.ForDisplay(),
)
}
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to query available provider packages",
fmt.Sprintf("Could not retrieve the list of available versions for provider %s: %s%s",
provider.ForDisplay(), err, suggestion,
),
))
case getproviders.ErrHostNoProviders:
switch {
case errorTy.Hostname == svchost.Hostname("github.com") && !errorTy.HasOtherVersion:
// If a user copies the URL of a GitHub repository into
// the source argument and removes the schema to make it
// provider-address-shaped then that's one way we can end up
// here. We'll use a specialized error message in anticipation
// of that mistake. We only do this if github.com isn't a
// provider registry, to allow for the (admittedly currently
// rather unlikely) possibility that github.com starts being
// a real Terraform provider registry in the future.
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid provider registry host",
fmt.Sprintf("The given source address %q specifies a GitHub repository rather than a Terraform provider. Refer to the documentation of the provider to find the correct source address to use.",
provider.String(),
),
))
case errorTy.HasOtherVersion:
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid provider registry host",
fmt.Sprintf("The host %q given in provider source address %q does not offer a Terraform provider registry that is compatible with this Terraform version, but it may be compatible with a different Terraform version.",
errorTy.Hostname, provider.String(),
),
))
default:
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid provider registry host",
fmt.Sprintf("The host %q given in provider source address %q does not offer a Terraform provider registry.",
errorTy.Hostname, provider.String(),
),
))
}
case getproviders.ErrRequestCanceled:
// We don't attribute cancellation to any particular operation,
// but rather just emit a single general message about it at
// the end, by checking ctx.Err().
default:
suggestion := fmt.Sprintf("\n\nTo see which modules are currently depending on %s and what versions are specified, run the following command:\n terraform providers", provider.ForDisplay())
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to query available provider packages",
fmt.Sprintf("Could not retrieve the list of available versions for provider %s: %s%s",
provider.ForDisplay(), err, suggestion,
),
))
}
}
}
// Returns a reused callback function for the QueryPackagesWarning event in a providercache.InstallerEvents struct.
func queryPackagesWarningCallback(diags *tfdiags.Diagnostics) func(provider addrs.Provider, warnings []string) {
return func(provider addrs.Provider, warnings []string) {
displayWarnings := make([]string, len(warnings))
for i, warning := range warnings {
displayWarnings[i] = fmt.Sprintf("- %s", warning)
}
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Additional provider information from registry",
fmt.Sprintf("The remote registry returned warnings for %s:\n%s",
provider.String(),
strings.Join(displayWarnings, "\n"),
),
))
}
}
// Returns a reused callback function for the LinkFromCacheFailure event in a providercache.InstallerEvents struct.
func linkFromCacheFailureCallback(diags *tfdiags.Diagnostics) func(provider addrs.Provider, version getproviders.Version, err error) {
return func(provider addrs.Provider, version getproviders.Version, err error) {
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to install provider from shared cache",
fmt.Sprintf("Error while importing %s v%s from the shared cache directory: %s.", provider.ForDisplay(), version, err),
))
}
}
// Returns a reused callback function for the FetchPackageFailure event in a providercache.InstallerEvents struct.
func fetchPackageFailureCallback(diags *tfdiags.Diagnostics, reqs getproviders.Requirements) func(provider addrs.Provider, version getproviders.Version, err error) {
return func(provider addrs.Provider, version getproviders.Version, err error) {
const summaryIncompatible = "Incompatible provider version"
switch err := err.(type) {
case getproviders.ErrProtocolNotSupported:
closestAvailable := err.Suggestion
switch {
case closestAvailable == getproviders.UnspecifiedVersion:
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
summaryIncompatible,
fmt.Sprintf(errProviderVersionIncompatible, provider.String()),
))
case version.GreaterThan(closestAvailable):
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
summaryIncompatible,
fmt.Sprintf(providerProtocolTooNew, provider.ForDisplay(),
version, tfversion.String(), closestAvailable, closestAvailable,
getproviders.VersionConstraintsString(reqs[provider]),
),
))
default: // version is less than closestAvailable
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
summaryIncompatible,
fmt.Sprintf(providerProtocolTooOld, provider.ForDisplay(),
version, tfversion.String(), closestAvailable, closestAvailable,
getproviders.VersionConstraintsString(reqs[provider]),
),
))
}
case getproviders.ErrPlatformNotSupported:
switch {
case err.MirrorURL != nil:
// If we're installing from a mirror then it may just be
// the mirror lacking the package, rather than it being
// unavailable from upstream.
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
summaryIncompatible,
fmt.Sprintf(
"Your chosen provider mirror at %s does not have a %s v%s package available for your current platform, %s.\n\nProvider releases are separate from Terraform CLI releases, so this provider might not support your current platform. Alternatively, the mirror itself might have only a subset of the plugin packages available in the origin registry, at %s.",
err.MirrorURL, err.Provider, err.Version, err.Platform,
err.Provider.Hostname,
),
))
default:
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
summaryIncompatible,
fmt.Sprintf(
"Provider %s v%s does not have a package available for your current platform, %s.\n\nProvider releases are separate from Terraform CLI releases, so not all providers are available for all platforms. Other versions of this provider may have different platforms supported.",
err.Provider, err.Version, err.Platform,
),
))
}
case getproviders.ErrRequestCanceled:
// We don't attribute cancellation to any particular operation,
// but rather just emit a single general message about it at
// the end, by checking ctx.Err().
default:
// We can potentially end up in here under cancellation too,
// in spite of our getproviders.ErrRequestCanceled case above,
// because not all of the outgoing requests we do under the
// "fetch package" banner are source metadata requests.
// In that case we will emit a redundant error here about
// the request being cancelled, but we'll still detect it
// as a cancellation after the installer returns and do the
// normal cancellation handling.
*diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to install provider",
fmt.Sprintf("Error while installing %s v%s: %s", provider.ForDisplay(), version, err),
))
}
}
}
// Returns a reused callback function for the FetchPackageSuccess event in a providercache.InstallerEvents struct.
func fetchPackageSuccessCallback(view views.Init) func(provider addrs.Provider, version getproviders.Version, localDir string, authResult *getproviders.PackageAuthenticationResult) {
return func(provider addrs.Provider, version getproviders.Version, localDir string, authResult *getproviders.PackageAuthenticationResult) {
var keyID string
if authResult != nil && authResult.ThirdPartySigned() {
keyID = authResult.KeyID
}
if keyID != "" {
keyID = view.PrepareMessage(views.KeyID, keyID)
}
view.LogInitMessage(views.InstalledProviderVersionInfo, provider.ForDisplay(), version, authResult, keyID)
}
}
// Returns a reused callback function for the ProvidersLockUpdated event in a providercache.InstallerEvents struct.
func providersLockUpdatedCallback(incompleteProviders *[]string) func(provider addrs.Provider, version getproviders.Version, localHashes []getproviders.Hash, signedHashes []getproviders.Hash, priorHashes []getproviders.Hash) {
return func(provider addrs.Provider, version getproviders.Version, localHashes []getproviders.Hash, signedHashes []getproviders.Hash, priorHashes []getproviders.Hash) {
// We're going to use this opportunity to track if we have any
// "incomplete" installs of providers. An incomplete install is
// when we are only going to write the local hashes into our lock
// file which means a `terraform init` command will fail in future
// when used on machines of a different architecture.
//
// We want to print a warning about this.
if len(signedHashes) > 0 {
// If we have any signedHashes hashes then we don't worry - as
// we know we retrieved all available hashes for this version
// anyway.
return
}
// If local hashes and prior hashes are exactly the same then
// it means we didn't record any signed hashes previously, and
// we know we're not adding any extra in now (because we already
// checked the signedHashes), so that's a problem.
//
// In the actual check here, if we have any priorHashes and those
// hashes are not the same as the local hashes then we're going to
// accept that this provider has been configured correctly.
if len(priorHashes) > 0 && !reflect.DeepEqual(localHashes, priorHashes) {
return
}
// Now, either signedHashes is empty, or priorHashes is exactly the
// same as our localHashes which means we never retrieved the
// signedHashes previously.
//
// Either way, this is bad. Let's complain/warn.
*incompleteProviders = append(*incompleteProviders, provider.ForDisplay())
}
}
// Returns a reused callback function for the ProvidersFetched event in a providercache.InstallerEvents struct.
func providersFetchedCallback(view views.Init) func(authResults map[addrs.Provider]*getproviders.PackageAuthenticationResult) {
return func(authResults map[addrs.Provider]*getproviders.PackageAuthenticationResult) {
thirdPartySigned := false
for _, authResult := range authResults {
if authResult.ThirdPartySigned() {
thirdPartySigned = true
break
}
}
if thirdPartySigned {
view.LogInitMessage(views.PartnerAndCommunityProvidersMessage)
}
}
}
const errInitCopyNotEmpty = `
The working directory already contains files. The -from-module option requires
an empty directory into which a copy of the referenced module will be placed.

@ -4,16 +4,20 @@
package command
import (
"context"
"errors"
"fmt"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/cloud"
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/command/views"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
@ -210,7 +214,7 @@ func (c *InitCommand) run(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, safeInitAction, stateStoreProviderAuthResult, configProviderDiags := c.getProvidersFromConfig(ctx, config, initArgs.Upgrade, initArgs.PluginPath, initArgs.Lockfile, view)
diags = diags.Append(configProviderDiags)
if configProviderDiags.HasErrors() {
view.Diagnostics(diags)
@ -220,7 +224,27 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int {
header = true
}
// The init command is not allowed to upgrade the provider used for PSS (unless we're reconfiguring the state store).
// Prompt the user about trusting the provider used for state storage.
// Course of action depends on the safeInitAction returned from getProvidersFromConfig
switch safeInitAction {
case SafeInitActionNotRelevant:
// do nothing; security features aren't relevant.
case SafeInitActionProceed:
// do nothing; provider is already trusted and there's no need to notify the user.
case SafeInitActionPromptForInput:
diags = diags.Append(c.promptStateStorageProviderApproval(config.Module.StateStore.ProviderAddr, configLocks, stateStoreProviderAuthResult))
if diags.HasErrors() {
view.Output(views.StateStoreProviderRejectedMessage)
view.Diagnostics(diags)
return 1
}
view.Output(views.StateStoreProviderApprovedMessage)
default:
// Handle SafeInitActionInvalid or unexpected action types
panic(fmt.Sprintf("When installing providers described in the config Terraform couldn't determine what 'safe init' action should be taken and returned action type %T. This is a bug in Terraform and should be reported.", safeInitAction))
}
// The init command is not allowed to upgrade the provider used for state storage (unless we're reconfiguring the state store).
// Unless users choose to reconfigure, they must upgrade the state store provider separately using `terraform state migrate -upgrade`.
if initArgs.Upgrade && !initArgs.Reconfigure && config.Module.StateStore != nil {
pAddr := config.Module.StateStore.ProviderAddr
@ -235,7 +259,7 @@ new lock: %#v`, pAddr.ForDisplay(), old, new))
// The upgrade has impacted the provider
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Cannot upgrade the provider used for pluggable state storage during \"terraform init -upgrade\"",
"Cannot upgrade the provider used for state storage during \"terraform init -upgrade\"",
fmt.Sprintf(`While upgrading providers Terraform attempted to upgrade the %s (%q) provider, which is used by the state_store block in your configuration.
Please use \"terraform state migrate -upgrade\" to upgrade the state store provider and navigate migrating your state between the two versions. You can then re-attempt \"terraform init -upgrade\" to upgrade the rest of your providers.
@ -332,7 +356,7 @@ If you do not intend to upgrade the state store provider, please update your con
view.Diagnostics(diags)
return 1
}
stateProvidersOutput, stateLocks, stateProvidersDiags := c.getProvidersFromState(ctx, state, configReqs, configLocks, initArgs.Upgrade, initArgs.PluginPath, initArgs.Lockfile, view)
stateProvidersOutput, stateLocks, stateProvidersDiags := c.getProvidersFromState(ctx, state, configReqs, configLocks, initArgs.PluginPath, view)
diags = diags.Append(stateProvidersDiags)
if stateProvidersDiags.HasErrors() {
view.Diagnostics(diags)
@ -397,3 +421,55 @@ If you do not intend to upgrade the state store provider, please update your con
}
return 0
}
// promptStateStorageProviderApproval is used when Terraform is unsure about the safety of the provider downloaded for state storage
// purposes, and we need to prompt the user to approve or reject using it.
func (c *InitCommand) promptStateStorageProviderApproval(stateStorageProvider addrs.Provider, configLocks *depsfile.Locks, authResult *getproviders.PackageAuthenticationResult) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
// If we can receive input then we prompt for ok from the user
lock := configLocks.Provider(stateStorageProvider)
var hashList strings.Builder
for _, hash := range lock.PreferredHashes() {
hashList.WriteString(fmt.Sprintf("- %s\n", hash))
}
var authentication string
if authResult != nil && authResult.KeyID != "" {
authentication = fmt.Sprintf("%s, key ID %s", authResult.String(), authResult.KeyID)
} else {
authentication = authResult.String()
}
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?
Platform: %s
Authentication: %s
Hashes:
%s
`,
lock.Provider().Type,
lock.Provider(),
lock.Version(),
getproviders.CurrentPlatform.String(),
authentication,
hashList.String(),
),
Description: fmt.Sprintf(`Check the details above for provider %q and confirm that you trust the provider.
Only 'yes' will be accepted to confirm.`, lock.Provider().Type),
})
if err != nil {
return diags.Append(fmt.Errorf("Failed to approve use of state storage provider: %s", err))
}
if v != "yes" {
return diags.Append(
fmt.Errorf("State store provider %q (%s) was not approved, so init cannot continue.",
lock.Provider().Type,
lock.Provider(),
),
)
}
return diags
}

@ -2560,7 +2560,7 @@ terraform {
t.Fatalf("command was not expected to complete successfully, but it did:\n%s", done(t).All())
}
output := done(t).Stderr()
expectedError := "Error: Cannot upgrade the provider used for pluggable state storage during \"terraform init -upgrade\""
expectedError := "Error: Cannot upgrade the provider used for state storage during \"terraform init -upgrade\""
if !strings.Contains(output, expectedError) {
t.Fatalf("expected error message not found:\n%s", output)
}
@ -3910,22 +3910,87 @@ 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("temporary: test showing use of HTTP server in mock provider source", func(t *testing.T) {
t.Run("no need to interactively approve a state store provider installed from local archive", 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)
// Mock provider still needs to be supplied via testingOverrides despite the mock HTTP source
// This mock provider source makes Terraform think the provider is coming from a local archive,
// so security checks are skipped.
source := newMockProviderSource(t, map[string][]string{
"hashicorp/test": {"1.2.3"},
})
mockProvider := mockPluggableStateStorageProvider()
mockProviderAddress := addrs.NewDefaultProvider("test")
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,
}
c := &InitCommand{
Meta: meta,
}
args := []string{
"-enable-pluggable-state-storage-experiment=true",
// This test doesn't need -safe-init in the flags due to the location of the provider
}
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 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 exist, but it doesn't")
}
})
t.Run("prompted to approve a state store provider downloaded via HTTP", 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)
// Set up mock provider source that mocks out downloading hashicorp/test v1.2.3 via HTTP.
// This stops Terraform auto-approving the provider installation.
source := newMockProviderSourceUsingTestHttpServer(t, map[string][]string{
// The test fixture config has no version constraints, so the latest version will
// be used; below 1.2.3 is the 'latest' version in the test world.
"hashicorp/test": {"1.0.0", "1.2.3"},
"hashicorp/test": {"1.2.3"},
})
mockProvider := mockPluggableStateStorageProvider()
mockProviderAddress := addrs.NewDefaultProvider("test")
// Allow the test to respond to the pause in provider installation for
// checking the state storage provider.
inputWriter := testInputMap(t, map[string]string{
"approve": "yes",
})
ui := new(cli.MockUi)
@ -3945,17 +4010,20 @@ 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",
}
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
// Check output via view
output := testOutput.All()
expectedOutputs := []string{
"Initializing the state store...",
"The state store provider was approved",
"Terraform has been successfully initialized!",
}
for _, expected := range expectedOutputs {
@ -3963,22 +4031,49 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) {
t.Fatalf("expected output to include %q, but got':\n %s", expected, output)
}
}
// Check output when prompting for approval
expectedInputPromptMsg := []string{
"Do you want to use provider \"test\" (registry.terraform.io/hashicorp/test), version 1.2.3, for managing state?",
getproviders.CurrentPlatform.String(),
"Authentication: verified checksum",
"h1:wlbEC2mChQZ2hhgUhl6SeVLPP7fMqOFUZAQhQ9GIIno=",
}
for _, expected := range expectedInputPromptMsg {
if !strings.Contains(inputWriter.String(), expected) {
t.Fatalf("expected the input prompt to include %q, but got':\n %s", expected, inputWriter.String())
}
}
// 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 exist, but it doesn't")
}
})
t.Run("temporary: test showing use of network mirror in mock provider source", func(t *testing.T) {
t.Run("approval prompt reports provider as unauthorized if no hashes returned from the HTTP mirror", 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)
// Mock provider still needs to be supplied via testingOverrides despite the mock network mirror
// The network mirror the provider will be downloaded from will not return any hashes, so
// Terraform won't have any way to check the provider's authenticity.
// This affects the prompt for approval, which this test case focuses on.
returnApprovedHashes := false
source := newHTTPMirrorProviderSourceUsingTestHttpServer(t, map[string][]string{
"hashicorp/test": {"1.2.3"},
}, returnApprovedHashes)
mockProvider := mockPluggableStateStorageProvider()
mockProviderAddress := addrs.NewDefaultProvider("test")
// Set up mock provider source that mocks out downloading hashicorp/test v1.2.3 from a network mirror.
source := newHTTPMirrorProviderSourceUsingTestHttpServer(t, map[string][]string{
"hashicorp/test": {"1.0.0", "1.2.3"},
}, true)
// Allow the test to respond to the pause in provider installation for
// checking the state storage provider.
inputWriter := testInputMap(t, map[string]string{
"approve": "yes",
})
ui := new(cli.MockUi)
view, done := testView(t)
@ -3997,18 +4092,221 @@ 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",
}
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
// Check output via view
output := testOutput.All()
expectedOutputs := []string{
"Initializing the state store...",
" Installed hashicorp/test v1.2.3 (verified checksum)", // verified checksum message due to hashes matching those described by the network mirror.
"The state store provider was approved",
"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)
}
}
// Check output when prompting for approval
expectedInputPromptMsg := []string{
"Do you want to use provider \"test\" (registry.terraform.io/hashicorp/test), version 1.2.3, for managing state?",
getproviders.CurrentPlatform.String(),
"Authentication: unauthenticated",
"h1:wlbEC2mChQZ2hhgUhl6SeVLPP7fMqOFUZAQhQ9GIIno=",
}
for _, expected := range expectedInputPromptMsg {
if !strings.Contains(inputWriter.String(), expected) {
t.Fatalf("expected the input prompt to include %q, but got':\n %s", expected, inputWriter.String())
}
}
// 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 exist, but it doesn't")
}
})
t.Run("users can reject a state store provider downloaded via HTTP", 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)
// Set up mock provider source that mocks out downloading hashicorp/test v1.2.3 via HTTP.
// This stops Terraform auto-approving the provider installation.
source := newMockProviderSourceUsingTestHttpServer(t, map[string][]string{
"hashicorp/test": {"1.2.3"},
})
mockProvider := mockPluggableStateStorageProvider()
mockProviderAddress := addrs.NewDefaultProvider("test")
// Allow the test to respond to the pause in provider installation for
// checking the state storage provider.
inputWriter := testInputMap(t, map[string]string{
"approve": "no",
})
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,
}
c := &InitCommand{
Meta: meta,
}
args := []string{
"-enable-pluggable-state-storage-experiment=true",
}
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 via view
output := testOutput.All()
expectedOutputs := []string{
"The state store provider was rejected",
}
for _, expected := range expectedOutputs {
if !strings.Contains(output, expected) {
t.Fatalf("expected output to include %q, but got':\n %s", expected, output)
}
}
// Check output when prompting for approval
expectedInputPromptMsg := []string{
"Do you want to use provider \"test\" (registry.terraform.io/hashicorp/test), version 1.2.3, for managing state?",
getproviders.CurrentPlatform.String(),
"Authentication: verified checksum",
"h1:wlbEC2mChQZ2hhgUhl6SeVLPP7fMqOFUZAQhQ9GIIno=",
}
for _, expected := range expectedInputPromptMsg {
if !strings.Contains(inputWriter.String(), expected) {
t.Fatalf("expected the input prompt to include %q, but got':\n %s", expected, inputWriter.String())
}
}
// Assert the dependency lock file was not 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 does")
}
})
t.Run("re-prompt to approve a provider after rejecting that provider in a previous init", 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)
// Set up mock provider source that mocks out downloading hashicorp/test v1.2.3 via HTTP.
// This stops Terraform auto-approving the provider installation.
mockProviderAddress := addrs.NewDefaultProvider("test")
mockProviderVersion := getproviders.MustParseVersion("1.2.3")
source := newMockProviderSourceUsingTestHttpServer(t, map[string][]string{
"hashicorp/test": {"1.2.3"},
})
// Set up providers for use in the second init attempt after the user adds the -safe-init flag.
mockProvider := mockPluggableStateStorageProvider()
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,
}
c := &InitCommand{
Meta: meta,
}
// Init number 1 - reject the provider
_ = testInputMap(t, map[string]string{
"approve": "no",
})
args := []string{
"-enable-pluggable-state-storage-experiment=true",
}
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())
}
output := testOutput.All()
expectedOutputs := []string{
"The state store provider was rejected",
}
for _, expectedOutput := range expectedOutputs {
if !strings.Contains(output, expectedOutput) {
t.Fatalf("expected output to include %q, but got':\n %s", expectedOutput, output)
}
}
// The rejected provider is present in the local cache after being rejected.
// However, this doesn't stop the user being prompted again.
cacheDir := meta.providerLocalCacheDir()
gotPackages := cacheDir.AllAvailablePackages()
wantPackages := map[addrs.Provider][]providercache.CachedProvider{
// "between" wasn't previously installed at all, so we installed
// the newest available version that matched the version constraints.
mockProviderAddress: {
{
Provider: mockProviderAddress,
Version: mockProviderVersion,
PackageDir: expectedPackageInstallPath(mockProviderAddress.Type, mockProviderVersion.String(), false),
},
},
}
if diff := cmp.Diff(wantPackages, gotPackages); diff != "" {
t.Errorf("wrong cache directory contents after upgrade\n%s", diff)
}
// Init number 2 - re-prompted for approval
_ = testInputMap(t, map[string]string{
"approve": "yes",
})
args = []string{
"-enable-pluggable-state-storage-experiment=true",
}
ui = new(cli.MockUi)
view, done = testView(t)
c.Ui = ui
c.View = view
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())
}
output = testOutput.All()
expectedOutputs = []string{
"Initializing the state store...",
"The state store provider was approved",
"Terraform has been successfully initialized!",
}
for _, expected := range expectedOutputs {

@ -199,6 +199,14 @@ var MessageRegistry map[InitMessageCode]InitMessage = map[InitMessageCode]InitMe
HumanValue: "\n[reset][bold]Initializing the state store...",
JSONValue: "Initializing the state store...",
},
"state_store_provider_approved_message": {
HumanValue: "\n[reset][bold]The state store provider was approved.",
JSONValue: "The state store provider was approved.",
},
"state_store_provider_rejected_message": {
HumanValue: "\n[reset][bold]The state store provider was rejected.",
JSONValue: "The state store provider was rejected.",
},
"dependencies_lock_changes_info": {
HumanValue: dependenciesLockChangesInfo,
JSONValue: dependenciesLockChangesInfo,
@ -339,6 +347,8 @@ const (
InitializingModulesMessage InitMessageCode = "initializing_modules_message"
InitializingBackendMessage InitMessageCode = "initializing_backend_message"
InitializingStateStoreMessage InitMessageCode = "initializing_state_store_message"
StateStoreProviderApprovedMessage InitMessageCode = "state_store_provider_approved_message"
StateStoreProviderRejectedMessage InitMessageCode = "state_store_provider_rejected_message"
InitializingProviderPluginFromConfigMessage InitMessageCode = "initializing_provider_plugin_from_config_message"
InitializingProviderPluginFromStateMessage InitMessageCode = "initializing_provider_plugin_from_state_message"
ReusingVersionIdentifiedFromConfig InitMessageCode = "reusing_version_during_state_provider_init"

Loading…
Cancel
Save