diff --git a/.changes/v1.16/NOTES-20260609-133222.yaml b/.changes/v1.16/NOTES-20260609-133222.yaml new file mode 100644 index 0000000000..2f3b4dfacc --- /dev/null +++ b/.changes/v1.16/NOTES-20260609-133222.yaml @@ -0,0 +1,5 @@ +kind: NOTES +body: 'command/init: Provider installation was changed to enable future enhancements in the area. This partially reverses the init event order changes from v1.15; module installation will now occur after the backend is initialized. The change should not have any significant end-user impact aside from the command output.' +time: 2026-06-09T13:32:22.033462+01:00 +custom: + Issue: "38699" diff --git a/internal/command/init.go b/internal/command/init.go index d30b970566..232ed77515 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -169,6 +169,12 @@ func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, ini view.Output(views.InitializingBackendMessage) } + earlyBdiags := c.earlyValidateBackend(root, initArgs) + diags = diags.Append(earlyBdiags) + if diags.HasErrors() { + return nil, true, diags + } + var opts *BackendOpts switch { case root.StateStore != nil: @@ -366,7 +372,7 @@ const ( // updated dependency lock data. 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) getProvidersFromPSSConfig(ctx context.Context, config *configs.Config, previousLocks *depsfile.Locks, upgrade bool, pluginDirs []string, flagLockfile string, view views.Init) (output bool, resultingLocks *depsfile.Locks, safeInitAction SafeInitAction, authResult *getproviders.PackageAuthenticationResult, diags tfdiags.Diagnostics) { +func (c *InitCommand) getProvidersFromPSSConfig(ctx context.Context, rootModEarly *configs.Module, previousLocks *depsfile.Locks, 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 for state store") defer span.End() @@ -379,23 +385,29 @@ func (c *InitCommand) getProvidersFromPSSConfig(ctx context.Context, config *con // So, we'll warn users about it to avoid later confusion when Terraform ends up using // a different provider than the lock file called for, or doesn't make expected changes // to the lock file. - // - // This warning is only shown once, here, to avoid duplication; not raised in getProvidersFromState. diags = diags.Append(c.providerDevOverrideInitWarnings()) diags = diags.Append(c.providerUnmanagedInitWarnings()) - // Collect the provider dependencies from the configuration. - allReqs, hclDiags := config.ProviderRequirements() - diags = diags.Append(hclDiags) - if hclDiags.HasErrors() { - return false, nil, SafeInitActionInvalid, nil, diags - } + // Collect the provider dependencies from the root module. + allReqs := rootModEarly.ProviderRequirements - // filter out only PSS providers from allReqs + // Get the state store provider from the root module's required providers. reqs := make(providerreqs.Requirements, 1) - for providerReq, cons := range allReqs { - if providerReq.Equals(config.Module.StateStore.ProviderAddr) { - reqs[providerReq] = cons + for providerReq := range maps.Values(allReqs.RequiredProviders) { + if providerReq.Type.Equals(rootModEarly.StateStore.ProviderAddr) { + con, err := providerreqs.ParseVersionConstraints(providerReq.Requirement.Required.String()) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid version constraint syntax for state store provider", + // The errors returned by ParseVersionConstraint already include + // the section of input that was incorrect, so we don't need to + // include that here. + Detail: fmt.Sprintf("Incorrect version constraint syntax: %s.", err.Error()), + Subject: providerReq.Requirement.DeclRange.Ptr(), + }) + } + reqs[providerReq.Type] = con } } @@ -448,7 +460,7 @@ func (c *InitCommand) getProvidersFromPSSConfig(ctx context.Context, config *con var stateStoreProviderAuthResult *getproviders.PackageAuthenticationResult evts := &providercache.InstallerEvents{ PendingProviders: func(reqs map[addrs.Provider]getproviders.VersionConstraints) { - view.Output(views.InitializingStateStoreProviderPluginMessage, config.Module.StateStore.Type) + view.Output(views.InitializingStateStoreProviderPluginMessage, rootModEarly.StateStore.Type) }, ProviderAlreadyInstalled: providerAlreadyInstalledCallback(view), BuiltInProviderAvailable: builtInProviderAvailableCallback(view), @@ -484,8 +496,8 @@ func (c *InitCommand) getProvidersFromPSSConfig(ctx context.Context, config *con 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()) + if rootModEarly.StateStore != nil && provider.Equals(rootModEarly.StateStore.ProviderAddr) { + log.Printf("[TRACE] getProvidersFromConfig: state storage provider %s (%q) auth result: %q", rootModEarly.StateStore.ProviderAddr.Type, rootModEarly.StateStore.ProviderAddr.ForDisplay(), stateStoreProviderAuthResult.String()) stateStoreProviderAuthResult = authResult } @@ -523,30 +535,30 @@ func (c *InitCommand) getProvidersFromPSSConfig(ctx context.Context, config *con } // Return advice to the calling code about what to do regarding safe init feature related to state storage providers - location, ok := providerLocations[config.Module.StateStore.ProviderAddr] + location, ok := providerLocations[rootModEarly.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 (getProvidersFromPSSConfig): 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) + log.Printf("[TRACE] init (getProvidersFromPSSConfig): 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.", rootModEarly.StateStore.ProviderAddr.Type, rootModEarly.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) + log.Printf("[TRACE] init (getProvidersFromConfig): the state storage provider %s (%q) will be changed in the dependency lock file during provider installation.", rootModEarly.StateStore.ProviderAddr.Type, rootModEarly.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) + log.Printf("[TRACE] init (getProvidersFromConfig): the state storage provider %s (%q) is downloaded from a local source, so we consider it safe.", rootModEarly.StateStore.ProviderAddr.Type, rootModEarly.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) + log.Printf("[DEBUG] init (getProvidersFromConfig): the state storage provider %s (%q) is downloaded via HTTP, so we consider it potentially unsafe.", rootModEarly.StateStore.ProviderAddr.Type, rootModEarly.StateStore.ProviderAddr) safeInitAction = SafeInitActionRequireApproval default: - panic(fmt.Sprintf("init (getProvidersFromConfig): unexpected provider location type for state storage provider %q: %T", config.Module.StateStore.ProviderAddr, location)) + panic(fmt.Sprintf("init (getProvidersFromConfig): unexpected provider location type for state storage provider %q: %T", rootModEarly.StateStore.ProviderAddr, location)) } } @@ -697,9 +709,9 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, // saveDependencyLockFile overwrites the contents of the dependency lock file. // 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) +func (c *InitCommand) saveDependencyLockFile(previousLocks, pssLock, providerLocks *depsfile.Locks, flagLockfile string, view views.Init) (output bool, diags tfdiags.Diagnostics) { + // Get the combination of locks from both potential provider download steps. + newLocks := c.mergeLockedDependencies(pssLock, providerLocks) // If the provider dependencies have changed since the last run then we'll // say a little about that in case the reader wasn't expecting a change. diff --git a/internal/command/init_run.go b/internal/command/init_run.go index dc9464c4c8..2f62e69a57 100644 --- a/internal/command/init_run.go +++ b/internal/command/init_run.go @@ -62,10 +62,6 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int { ctx, done := c.InterruptibleContext(c.CommandContext()) defer done() - // This will track whether we outputted anything so that we know whether - // to output a newline before the success message - var header bool - if initArgs.FromModule != "" { src := initArgs.FromModule @@ -82,7 +78,6 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int { } view.Output(views.CopyingConfigurationMessage, src) - header = true hooks := uiModuleInstallHooks{ Ui: c.Ui, @@ -162,71 +157,19 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int { return 1 } - if rootModEarly.StateStore != nil { // We know rootModEarly is not nil. - rootModEarly.StateStore.ProviderSupplyMode = c.Meta.getProviderSupplyModeForStateStore(rootModEarly) - if rootModEarly.StateStore.ProviderSupplyMode == getproviders.Unset { - panic("unset provider supply mode for state store") - } - } - - if initArgs.Get { - modsOutput, modsAbort, modsDiags := c.getModules(ctx, path, initArgs.TestsDirectory, rootModEarly, initArgs.Upgrade, view) - diags = diags.Append(modsDiags) - if modsAbort || modsDiags.HasErrors() { - view.Diagnostics(diags) - return 1 - } - if modsOutput { - header = true - } - } - - // With all of the modules (hopefully) installed, we can now try to load the - // whole configuration tree. - config, confDiags := c.loadConfigWithTests(path, initArgs.TestsDirectory) - if config != nil && config.Module != nil && config.Module.StateStore != nil { - config.Module.StateStore.ProviderSupplyMode = c.Meta.getProviderSupplyModeForStateStore(config.Module) - if config.Module.StateStore.ProviderSupplyMode == getproviders.Unset { - panic("unset provider supply mode for state store") - } - } - // configDiags will be handled after: - // - the version constraint check has happened - // - and, the backend/state_store is initialised - - // Before we go further, we'll check to make sure none of the modules in - // the configuration declare that they don't support this Terraform - // version, so we can produce a version-related error message rather than - // potentially-confusing downstream errors. - versionDiags := terraform.CheckCoreVersionRequirements(config) - if versionDiags.HasErrors() { - view.Diagnostics(versionDiags) - return 1 - } - - earlyBdiags := c.earlyValidateBackend(rootModEarly, initArgs) - diags = diags.Append(earlyBdiags) - - // We've passed the core version check, now we can show errors from the early configuration. - // This prevents trying to initialise the backend with faulty configuration. - if earlyConfDiags.HasErrors() || earlyBdiags.HasErrors() { - diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError)), earlyConfDiags) - view.Diagnostics(diags) - return 1 - } - - // Now the full configuration is loaded, we can download the providers specified in the configuration. - // This is step one of a two-step provider download process - // Providers may be downloaded by this code, but the dependency lock file is only updated later in `init` - // after step two of provider download is complete. - previousLocks, moreDiags := c.lockedDependencies() - diags = diags.Append(moreDiags) - // If -state-provider-lock-file is set, we'll use that to obtain a new lock used for the state store provider // This will be 'upserted': it may be that the previous locks don't contain the provider being added. potentially due to being empty, or contain a different version. // The lock added will be used in the first step of provider download. // - // We leave `previousLocks` unchanged so it can be used to accurately detect changes to the locks when the lock file is updated later. + // We load locks from any pre-existing dependency lock file. These may or may not be altered by the -state-provider-lock-file flag. + // The altered copy of the locks will be used to influence subsequent provider download steps. + // The unaltered copy of the locks will be used at the end of the run to determine whether we need to update the dependency lock file on disk. + previousLocks, locksDiags := c.lockedDependencies() + diags = diags.Append(locksDiags) + if locksDiags.HasErrors() { + view.Diagnostics(diags) + return 1 + } alteredPreviousLocks := previousLocks.DeepCopy() if initArgs.StateStoreProviderLockFile != "" { stateStoreLocks, lockDiags := depsfile.LoadLocksFromFile(initArgs.StateStoreProviderLockFile) @@ -241,14 +184,14 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int { } diags = diags.Append(lockDiags) // capture any warnings - lock := stateStoreLocks.Provider(config.Module.StateStore.ProviderAddr) + lock := stateStoreLocks.Provider(rootModEarly.StateStore.ProviderAddr) if lock == nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "State store provider not found in -state-provider-lock-file dependency lock file", fmt.Sprintf("Terraform could not find the state store provider %q (%s) in the dependency lock file %q provided via the -state-provider-lock-file flag. Please ensure the lock file contains a lock for the state store provider and try again.", - config.Module.StateStore.ProviderAddr.Type, - config.Module.StateStore.ProviderAddr.ForDisplay(), + rootModEarly.StateStore.ProviderAddr.Type, + rootModEarly.StateStore.ProviderAddr.ForDisplay(), initArgs.StateStoreProviderLockFile, ), )) @@ -265,8 +208,8 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int { ) } - var pssLocks *depsfile.Locks - if config != nil && config.Module != nil && config.Module.StateStore != nil { + var pssLocks *depsfile.Locks // May end up containing 0 or 1 lock. + if rootModEarly.StateStore != nil { var configProvidersOutput bool var safeInitAction SafeInitAction var stateStoreProviderAuthResult *getproviders.PackageAuthenticationResult @@ -286,22 +229,25 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int { fmt.Sprintf(`Terraform will not upgrade the %s (%q) provider as part of this operation because it is used for state storage. Please use \"terraform state migrate -upgrade\" to upgrade the state store provider and navigate migrating your state between the two versions.`, - config.Module.StateStore.ProviderAddr.Type, - config.Module.StateStore.ProviderAddr.ForDisplay(), + rootModEarly.StateStore.ProviderAddr.Type, + rootModEarly.StateStore.ProviderAddr.ForDisplay(), ), ), ) } } - configProvidersOutput, pssLocks, safeInitAction, stateStoreProviderAuthResult, configProviderDiags = c.getProvidersFromPSSConfig(ctx, config, alteredPreviousLocks, allowUpgrade, initArgs.PluginPath, initArgs.Lockfile, view) + // Use alteredPreviousLocks, which may contain an additional lock supplied from the -state-provider-lock-file flag + configProvidersOutput, pssLocks, safeInitAction, stateStoreProviderAuthResult, configProviderDiags = c.getProvidersFromPSSConfig(ctx, rootModEarly, alteredPreviousLocks, allowUpgrade, initArgs.PluginPath, initArgs.Lockfile, view) diags = diags.Append(configProviderDiags) if configProviderDiags.HasErrors() { view.Diagnostics(diags) return 1 } if configProvidersOutput { - header = true + // If we outputted information, then we need to output a newline + // so that our success message is nicely spaced out from prior text. + view.Output(views.EmptyMessage) } // Course of action depends on the safeInitAction returned from getProvidersFromPSSConfig @@ -311,7 +257,7 @@ Please use \"terraform state migrate -upgrade\" to upgrade the state store provi case SafeInitActionRequireApproval: if c.input { // Prompt the user about trusting the provider used for state storage. - diags = diags.Append(c.promptStateStorageProviderApproval(config.Module.StateStore.ProviderAddr, pssLocks, stateStoreProviderAuthResult)) + diags = diags.Append(c.promptStateStorageProviderApproval(rootModEarly.StateStore.ProviderAddr, pssLocks, stateStoreProviderAuthResult)) if diags.HasErrors() { view.Output(views.StateStoreProviderInteractiveRejectedMessage) view.Diagnostics(diags) @@ -321,7 +267,7 @@ Please use \"terraform state migrate -upgrade\" to upgrade the state store provi } else { // Confirm that a lock was used to control download. // Note: we have to wait and do that here because at this point we know the provider was downloaded from a source that requires additional info about trust. - if alteredPreviousLocks.Provider(config.Module.StateStore.ProviderAddr) == nil { + if alteredPreviousLocks.Provider(rootModEarly.StateStore.ProviderAddr) == nil { // No lock was provided for the state store provider either through pre-existing locks or through the -state-provider-lock-file flag. diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -337,12 +283,12 @@ Please use \"terraform state migrate -upgrade\" to upgrade the state store provi // 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)) } - } - // 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 { - view.Output(views.EmptyMessage) + // Record how the state store provider is supplied to Terraform + rootModEarly.StateStore.ProviderSupplyMode = c.Meta.getProviderSupplyModeForStateStore(rootModEarly) + if rootModEarly.StateStore.ProviderSupplyMode == getproviders.Unset { + panic("unset provider supply mode for state store") + } } var back backend.Backend @@ -359,32 +305,11 @@ Please use \"terraform state migrate -upgrade\" to upgrade the state store provi back, backDiags = c.Meta.backendFromState(ctx) } if backendOutput { - header = true - } - if header { // If we outputted information, then we need to output a newline // so that our success message is nicely spaced out from prior text. view.Output(views.EmptyMessage) } - // Show any errors from initializing the backend. - // No preamble using `InitConfigError` is present, as we expect - // any errors to from configuring the backend itself. - diags = diags.Append(backDiags) - if backDiags.HasErrors() { - view.Diagnostics(diags) - return 1 - } - - // If everything is ok with the core version check and backend/state_store initialization, - // show other errors from loading the full configuration tree. - diags = diags.Append(confDiags) - if confDiags.HasErrors() { - diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError))) - view.Diagnostics(diags) - return 1 - } - var state *states.State // If we have a functional backend (either just initialized or initialized @@ -414,47 +339,99 @@ Please use \"terraform state migrate -upgrade\" to upgrade the state store provi state = sMgr.State() } - stateProvidersOutput, stateLocks, stateProvidersDiags := c.getProviders(ctx, config, state, initArgs.Upgrade, pssLocks, initArgs.PluginPath, view) + if initArgs.Get { + modsOutput, modsAbort, modsDiags := c.getModules(ctx, path, initArgs.TestsDirectory, rootModEarly, initArgs.Upgrade, view) + diags = diags.Append(modsDiags) + if modsAbort || modsDiags.HasErrors() { + view.Diagnostics(diags) + return 1 + } + if modsOutput { + // If we outputted information, then we need to output a newline + // so that our success message is nicely spaced out from prior text. + view.Output(views.EmptyMessage) + } + } + + // With all of the modules (hopefully) installed, we can now try to load the + // whole configuration tree. + config, confDiags := c.loadConfigWithTests(path, initArgs.TestsDirectory) + // configDiags will be handled after the version constraint check, since an + // incorrect version of terraform may produce errors for configuration + // constructs added in later versions. + + // Before we go further, we'll check to make sure none of the modules in + // the configuration declare that they don't support this Terraform + // version, so we can produce a version-related error message rather than + // potentially-confusing downstream errors. + versionDiags := terraform.CheckCoreVersionRequirements(config) + if versionDiags.HasErrors() { + view.Diagnostics(versionDiags) + return 1 + } + + // We've passed the core version check, now we can show any errors related to configuration + // 1. Early errors from parsing the root module. + // 2. Show any errors from initializing the backend. + diags = diags.Append(earlyConfDiags) + diags = diags.Append(backDiags) + if earlyConfDiags.HasErrors() { + diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError))) + view.Diagnostics(diags) + return 1 + } + // If there are only backend errors, we won't show the InitConfigError preamble; + // the config isn't the source of the errors it's probably the backend's own + // Configure logic. + if backDiags.HasErrors() { + view.Diagnostics(diags) + return 1 + } + + // 3. Show any errors from loading the full configuration tree. + diags = diags.Append(confDiags) + if confDiags.HasErrors() { + diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError))) + view.Diagnostics(diags) + return 1 + } + + if cb, ok := back.(*cloud.Cloud); ok { + if c.RunningInAutomation { + if err := cb.AssertImportCompatible(config); err != nil { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Compatibility error", err.Error())) + view.Diagnostics(diags) + return 1 + } + } + } + + // Proceed with downloading providers + stateProvidersOutput, providerLocks, stateProvidersDiags := c.getProviders(ctx, config, state, initArgs.Upgrade, pssLocks, initArgs.PluginPath, view) diags = diags.Append(stateProvidersDiags) if stateProvidersDiags.HasErrors() { view.Diagnostics(diags) return 1 } if stateProvidersOutput { - header = true - } - if header { // If we outputted information, then we need to output a newline // so that our success message is nicely spaced out from prior text. view.Output(views.EmptyMessage) } - // Now the two steps of provider download have happened, update the dependency lock file if it has changed. - lockFileOutput, lockFileDiags := c.saveDependencyLockFile(previousLocks, pssLocks, stateLocks, initArgs.Lockfile, view) + // Update the dependency lock file, if it has changed. + lockFileOutput, lockFileDiags := c.saveDependencyLockFile(previousLocks, pssLocks, providerLocks, initArgs.Lockfile, view) diags = diags.Append(lockFileDiags) if lockFileDiags.HasErrors() { view.Diagnostics(diags) return 1 } if lockFileOutput { - header = true - } - if header { // If we outputted information, then we need to output a newline // so that our success message is nicely spaced out from prior text. view.Output(views.EmptyMessage) } - if cb, ok := back.(*cloud.Cloud); ok { - if c.RunningInAutomation { - if err := cb.AssertImportCompatible(config); err != nil { - diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Compatibility error", err.Error())) - view.Diagnostics(diags) - return 1 - } - } - } - // If we accumulated any warnings along the way that weren't accompanied // by errors then we'll output them here so that the success message is // still the final thing shown. diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 053f1861f1..d17ebbc9ba 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -4780,6 +4780,97 @@ func TestInit_stateStore_newWorkingDir_interactiveProviderApproval(t *testing.T) }) } +// Test what happens when a child module also uses the provider for state storage and has a version constraint +// that doesn't match the version constraint in the root module. We need the state store provider to be installed before +// child modules are downloaded, so in this scenario we expect an error to happen. +func TestInit_stateStore_versionConstraintChildModule(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("init-with-state-store-in-child-module"), td) + t.Chdir(td) + + source := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": { + "1.0.0", // Satisfies constraint in root module only + "2.0.0", // Satisfies constraint in child module only + }, + }) + + 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", + } + 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- this must be done separately for stdout and stderr due to + // the interleaving of output caused in tests by (TestOutput).All() + + // Check stdout + stdout := testOutput.Stdout() + expectedOutput := `Initializing provider plugin for state store "test_store"... +- Finding hashicorp/test versions matching "< 2.0.0"... +- Installing hashicorp/test v1.0.0... +- Installed hashicorp/test v1.0.0 (verified checksum) + +Initializing the state store "test_store"... + +Initializing modules... +- child in child + +Initializing provider plugins... +- Reusing previous version of hashicorp/test from the dependency lock file +` + + if stdout != expectedOutput { + t.Errorf("didn't get expected output") + diff := cmp.Diff(expectedOutput, stdout) + t.Fatalf("unexpected diff in output:\n%s", diff) + } + + // Check stderr + stderr := testOutput.Stderr() + expectedError := ` +Error: Failed to query available provider packages + +Could not retrieve the list of available versions for provider +hashicorp/test: locked provider registry.terraform.io/hashicorp/test 1.0.0 +does not match configured version constraint >= 2.0.0, < 2.0.0; must use +terraform init -upgrade to allow selection of new versions + +To see which modules are currently depending on hashicorp/test and what +versions are specified, run the following command: + terraform providers +` + + if stderr != expectedError { + t.Errorf("didn't get expected error output") + diff := cmp.Diff(expectedError, stderr) + t.Fatalf("unexpected diff in error output:\n%s", diff) + } +} + // Testing init's behaviors when, in automation, we're approving a new state store provider when a workspace is initialized for the first time. func TestInit_stateStore_newWorkingDir_inAutomationProviderApproval(t *testing.T) { t.Run("users do not need to approve trusting a state store provider if it's installed from local archive", func(t *testing.T) { diff --git a/internal/command/testdata/init-get/output.jsonlog b/internal/command/testdata/init-get/output.jsonlog index 31c3b0d137..88acf532fd 100644 --- a/internal/command/testdata/init-get/output.jsonlog +++ b/internal/command/testdata/init-get/output.jsonlog @@ -1,7 +1,7 @@ {"@level":"info","@message":"Terraform 1.9.0-dev","@module":"terraform.ui","terraform":"1.9.0-dev","type":"version","ui":"1.2"} +{"@level":"info","@message":"Initializing the backend...","@module":"terraform.ui","message_code": "initializing_backend_message","type":"init_output"} {"@level":"info","@message":"Initializing modules...","@module":"terraform.ui","message_code": "initializing_modules_message","type":"init_output"} {"@level":"info","@message":"- foo in foo","@module":"terraform.ui","type":"log"} -{"@level":"info","@message":"Initializing the backend...","@module":"terraform.ui","message_code": "initializing_backend_message","type":"init_output"} {"@level":"info","@message":"Initializing provider plugins...","@module":"terraform.ui","message_code": "initializing_provider_plugin_message","type":"init_output"} {"@level":"info","@message":"Terraform has been successfully initialized!","@module":"terraform.ui","message_code": "output_init_success_message","type":"init_output"} {"@level":"info","@message":"You may now begin working with Terraform. Try running \"terraform plan\" to see\nany changes that are required for your infrastructure. All Terraform commands\nshould now work.\n\nIf you ever set or change modules or backend configuration for Terraform,\nrerun this command to reinitialize your working directory. If you forget, other\ncommands will detect it and remind you to do so if necessary.","@module":"terraform.ui","message_code": "output_init_success_cli_message","type":"init_output"} diff --git a/internal/command/testdata/init-with-state-store-in-child-module/child/main.tf b/internal/command/testdata/init-with-state-store-in-child-module/child/main.tf new file mode 100644 index 0000000000..598c7563fa --- /dev/null +++ b/internal/command/testdata/init-with-state-store-in-child-module/child/main.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + version = ">=2.0.0" + } + } +} + +resource "test_resource" "example" { + string = "Hello, world!" +} diff --git a/internal/command/testdata/init-with-state-store-in-child-module/main.tf b/internal/command/testdata/init-with-state-store-in-child-module/main.tf new file mode 100644 index 0000000000..2a3fd13ec5 --- /dev/null +++ b/internal/command/testdata/init-with-state-store-in-child-module/main.tf @@ -0,0 +1,19 @@ +terraform { + + required_providers { + test = { + source = "hashicorp/test" + version = "<2.0.0" // mutually exclusive with the version constraint in the child module, which should cause an error during init + } + } + state_store "test_store" { + provider "test" { + } + + value = "foobar" + } +} + +module "child" { + source = "./child" +}