diff --git a/internal/command/init.go b/internal/command/init.go index 3494b00d3e..8d9ddb8161 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -392,7 +392,7 @@ const ( // The dependency lock file itself isn't updated here. // // Calling code is responsible for validating inputs to this method, e.g. mutually exclusive flags. -func (c *InitCommand) getProvidersFromConfig(ctx context.Context, config *configs.Config, upgrade bool, pluginDirs []string, flagLockfile string, view views.Init, installerHook *providerPolicyHook) (output bool, resultingLocks *depsfile.Locks, safeInitAction SafeInitAction, authResult *getproviders.PackageAuthenticationResult, diags tfdiags.Diagnostics) { +func (c *InitCommand) getProvidersFromConfig(ctx context.Context, config *configs.Config, upgrade bool, pluginDirs []string, flagLockfile string, view views.Init, installerHook ...providercache.InstallerHook) (output bool, resultingLocks *depsfile.Locks, safeInitAction SafeInitAction, authResult *getproviders.PackageAuthenticationResult, diags tfdiags.Diagnostics) { if config == nil { return false, nil, SafeInitActionNotRelevant, nil, diags } @@ -530,7 +530,7 @@ func (c *InitCommand) getProvidersFromConfig(ctx context.Context, config *config // Determine which required providers are already downloaded, and download any // new providers or newer versions of providers - configLocks, installErr := inst.EnsureProviderVersions(ctx, previousLocks, reqs, mode, installerHook) + configLocks, installErr := inst.EnsureProviderVersions(ctx, previousLocks, reqs, mode, installerHook...) if ctx.Err() == context.Canceled { diags = diags.Append(fmt.Errorf("Provider installation was canceled by an interrupt signal.")) view.Diagnostics(diags) // TODO: Why is the output viewed here? @@ -589,7 +589,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, pluginDirs []string, view views.Init, installerHook *providerPolicyHook) (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, installerHooks ...providercache.InstallerHook) (output bool, resultingLocks *depsfile.Locks, diags tfdiags.Diagnostics) { ctx, span := tracer.Start(ctx, "install providers from state") defer span.End() @@ -703,7 +703,7 @@ func (c *InitCommand) getProvidersFromState(ctx context.Context, state *states.S // would remove the effects of version constraints from the config. // > Any validation of CLI flag usage is already done in getProvidersFromConfig - newLocks, err := inst.EnsureProviderVersions(ctx, inProgressLocks, reqs, mode, installerHook) + newLocks, err := inst.EnsureProviderVersions(ctx, inProgressLocks, reqs, mode, installerHooks...) if ctx.Err() == context.Canceled { diags = diags.Append(fmt.Errorf("Provider installation was canceled by an interrupt signal.")) view.Diagnostics(diags) diff --git a/internal/command/init_run.go b/internal/command/init_run.go index a4cfc5aad5..168c683830 100644 --- a/internal/command/init_run.go +++ b/internal/command/init_run.go @@ -18,8 +18,10 @@ import ( "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/getproviders" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/policy" + "github.com/hashicorp/terraform/internal/providercache" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" @@ -242,6 +244,8 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int { previousLocks, moreDiags := c.lockedDependencies() diags = diags.Append(moreDiags) + installerHooks := []providercache.InstallerHook{} + reqsByModule, reqDiags := config.ProviderRequirementsByModule() if reqDiags.HasErrors() { view.Diagnostics(diags.Append(reqDiags)) @@ -254,8 +258,25 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int { policyResults: policyResults, config: config, } + installerHooks = append(installerHooks, providerHook) + + if config != nil && config.Module != nil && config.Module.StateStore != nil { + lock := previousLocks.Provider(config.Module.StateStore.ProviderAddr) + var priorVersion *providerreqs.Version + if lock != nil { + v := lock.Version() + priorVersion = &v + } + stateStorageHook := &stateStorageProviderInstallHook{ + provider: config.Module.StateStore.ProviderAddr, + priorVersion: priorVersion, + supplyMode: config.Module.StateStore.ProviderSupplyMode, + reconfigure: initArgs.Reconfigure, + } + installerHooks = append(installerHooks, stateStorageHook) + } - configProvidersOutput, configLocks, safeInitAction, stateStoreProviderAuthResult, configProviderDiags := c.getProvidersFromConfig(ctx, config, initArgs.Upgrade, initArgs.PluginPath, initArgs.Lockfile, view, providerHook) + configProvidersOutput, configLocks, safeInitAction, stateStoreProviderAuthResult, configProviderDiags := c.getProvidersFromConfig(ctx, config, initArgs.Upgrade, initArgs.PluginPath, initArgs.Lockfile, view, installerHooks...) diags = diags.Append(configProviderDiags) if configProviderDiags.HasErrors() { view.PolicyResults(policyResults) @@ -286,35 +307,6 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int { 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 - old := previousLocks.Provider(pAddr) - new := configLocks.Provider(pAddr) - if old == nil || new == nil { - panic(fmt.Sprintf(`Unexpected missing provider lock for %s during init -upgrade: -prior lock: %#v -new lock: %#v`, pAddr.ForDisplay(), old, new)) - } - if !new.Version().Same((old.Version())) { - // The upgrade has impacted the provider - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "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. - -If you do not intend to upgrade the state store provider, please update your configuration to pin to the current version (%s), and re-run \"terraform init -upgrade\" to upgrade the rest of your providers. -`, - pAddr.Type, pAddr.ForDisplay(), old.Version()), - ), - ) - view.Diagnostics(diags) - return 1 - } - } - // If we outputted information, then we need to output a newline // so that our success message is nicely spaced out from prior text. if header { diff --git a/internal/command/init_test.go b/internal/command/init_test.go index f3c1b5b003..bbd79d1263 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -2566,18 +2566,12 @@ terraform { } // Assert that no providers were upgraded. - // - // However, "test" v9.9.9 would be installed in the cache, because the error occurs after the upgrade - // process identifies that provider as a candidate for upgrade. + // Also, no packages were downloaded. cacheDir := m.providerLocalCacheDir() gotPackages := cacheDir.AllAvailablePackages() wantPackages := map[addrs.Provider][]providercache.CachedProvider{ addrs.NewDefaultProvider("test"): { - { - Provider: addrs.NewDefaultProvider("test"), - Version: getproviders.MustParseVersion("9.9.9"), - PackageDir: expectedPackageInstallPath("test", "9.9.9", false), - }, + // no 9.9.9 entry { Provider: addrs.NewDefaultProvider("test"), Version: getproviders.MustParseVersion("1.2.3"), diff --git a/internal/command/meta_pluggable_state_storage.go b/internal/command/meta_pluggable_state_storage.go new file mode 100644 index 0000000000..681d12f3b8 --- /dev/null +++ b/internal/command/meta_pluggable_state_storage.go @@ -0,0 +1,59 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package command + +import ( + "context" + "fmt" + + tfaddr "github.com/hashicorp/terraform-registry-address" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/getproviders" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" + "github.com/hashicorp/terraform/internal/providercache" +) + +var _ providercache.InstallerHook = stateStorageProviderInstallHook{} + +type stateStorageProviderInstallHook struct { + provider tfaddr.Provider + priorVersion *providerreqs.Version + supplyMode getproviders.ProviderSupplyMode + reconfigure bool +} + +func (h stateStorageProviderInstallHook) ProviderVersionSelected(ctx context.Context, provider addrs.Provider, version string) error { + if !provider.Equals(h.provider) { + return nil // irrelevant + } + + if h.priorVersion == nil { + return nil // not an upgrade, install for first time + } + + // if h.supplyMode != getproviders.ManagedByTerraform { + // return nil // not managed by Terraform, so upgrades won't change this provider + // } + + if h.reconfigure { + return nil // user has opted out of state migration so no error + } + + v := providerreqs.MustParseVersion(version) + if v.Same(*h.priorVersion) { + return nil // not an upgrade, same version selected + } + + return fmt.Errorf(`Cannot upgrade the provider used for state storage during "terraform init -upgrade". + +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. + +If you do not intend to upgrade the state store provider, please update your configuration to pin to the current version (%s), and re-run "terraform init -upgrade" to upgrade the rest of your providers. +`, + provider.Type, + provider.ForDisplay(), + h.priorVersion, + ) +} diff --git a/internal/providercache/installer.go b/internal/providercache/installer.go index bf4606eccf..f35d2c5ff6 100644 --- a/internal/providercache/installer.go +++ b/internal/providercache/installer.go @@ -350,7 +350,6 @@ NeedProvider: // We do this before checking the lock file, so that we also // evaluate policy for providers that are already installed. err := hook.ProviderVersionSelected(ctx, provider, version.String()) - // return a generic error here that the init command returns to the CLI. // The detailed policy diagnostics are included in the policy results // and will be formatted in the CLI output.