-
Notifications
You must be signed in to change notification settings - Fork 10.3k
PSS: Add alternative, experimental version of init command that downloads providers in two stages
#37350
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
SarahFrench
merged 33 commits into
main
from
pss/multistage-provider-download-experiment
Aug 18, 2025
Merged
PSS: Add alternative, experimental version of init command that downloads providers in two stages
#37350
Changes from all commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
025f225
Add forked version of `run` logic that's only used if experiments are…
SarahFrench 58b4ee7
Reorder actions in experimental init - load in full config before con…
SarahFrench a214b41
Add getProvidersFromConfig method, initially as an exact copy of getP…
SarahFrench 37352b3
Make getProvidersFromConfig not use state to get providers
SarahFrench 143c6d0
Add `appendLockedDependencies` method to `Meta` to allow multi-phase …
SarahFrench 0875016
Update experimental init to use new getProvidersFromConfig method
SarahFrench c41ac8c
Add new getProvidersFromState method that only accepts state informat…
SarahFrench 4cacf15
Update messages sent to view about provider download phases
SarahFrench a85eee9
Change init to save updates to the deps lock file only once
SarahFrench 90bb924
Make Terraform output report that a lock file _will_ be made after pr…
SarahFrench 4714764
Remove use of `ProviderDownloadOutcome`s
SarahFrench 07cf87c
Move repeated code into separate method
SarahFrench f74c4f0
Change provider download approach: determine if locks changed at poin…
SarahFrench 98c4677
Refactor `mergeLockedDependencies` and update test
SarahFrench 952e14c
Add comments to provider download methods
SarahFrench f40917b
Fix issue where incorrect message ouput to view when downloading prov…
SarahFrench 20f2491
Update `mergeLockedDependencies` method to be more generic
SarahFrench 4ea7c2b
Update `getProvidersFromState` method to receive in-progress config l…
SarahFrench dcf6d39
Fix config for `TestInit_stateStoreBlockIsExperimental`
SarahFrench 5556043
Improve testing of mergeLockedDependencies; state locks are always mi…
SarahFrench 3889abe
Add tests for 2 phase provider download
SarahFrench b05865e
Add test case to cover use of the `-upgrade` flag
SarahFrench 9c948e5
Change the message shown when a provider is reused during the second …
SarahFrench 88cfedb
Update mergeLockedDependencies comment
SarahFrench 3129ef4
fix: completely remove use of upgrade flag in getProvidersFromState
SarahFrench 2665767
Fix: avoid nil pointer errors by returning an empty collection of loc…
SarahFrench 03adc1d
Fix: use state store data only in diagnostic
SarahFrench 63a9b92
Change how we make PSS experimental - avoid relying on a package leve…
SarahFrench 1ea29ca
Remove full-stop in view message, update tests
SarahFrench c82186b
Update span names to be unique
SarahFrench 14ba3e7
Re-add lost early returns
SarahFrench da3e372
Remove unused view messages
SarahFrench 31f2cc1
Add comments to new view messages
SarahFrench File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,346 @@ | ||
| // Copyright (c) HashiCorp, Inc. | ||
| // SPDX-License-Identifier: BUSL-1.1 | ||
|
|
||
| package command | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "strings" | ||
|
|
||
| "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/states" | ||
| "github.com/hashicorp/terraform/internal/terraform" | ||
| "github.com/hashicorp/terraform/internal/tfdiags" | ||
| "go.opentelemetry.io/otel/attribute" | ||
| "go.opentelemetry.io/otel/codes" | ||
| "go.opentelemetry.io/otel/trace" | ||
| ) | ||
|
|
||
| // `runPssInit` is an altered version of the logic in `run` that contains changes | ||
| // related to the PSS project. This is used by the (InitCommand.Run method only if Terraform has | ||
| // experimental features enabled. | ||
| func (c *InitCommand) runPssInit(initArgs *arguments.Init, view views.Init) int { | ||
| var diags tfdiags.Diagnostics | ||
|
|
||
| c.forceInitCopy = initArgs.ForceInitCopy | ||
| c.Meta.stateLock = initArgs.StateLock | ||
| c.Meta.stateLockTimeout = initArgs.StateLockTimeout | ||
| c.reconfigure = initArgs.Reconfigure | ||
| c.migrateState = initArgs.MigrateState | ||
| c.Meta.ignoreRemoteVersion = initArgs.IgnoreRemoteVersion | ||
| c.Meta.input = initArgs.InputEnabled | ||
| c.Meta.targetFlags = initArgs.TargetFlags | ||
| c.Meta.compactWarnings = initArgs.CompactWarnings | ||
|
|
||
| varArgs := initArgs.Vars.All() | ||
| items := make([]arguments.FlagNameValue, len(varArgs)) | ||
| for i := range varArgs { | ||
| items[i].Name = varArgs[i].Name | ||
| items[i].Value = varArgs[i].Value | ||
| } | ||
| c.Meta.variableArgs = arguments.FlagNameValueSlice{Items: &items} | ||
|
|
||
| // Copying the state only happens during backend migration, so setting | ||
| // -force-copy implies -migrate-state | ||
| if c.forceInitCopy { | ||
| c.migrateState = true | ||
| } | ||
|
|
||
| if len(initArgs.PluginPath) > 0 { | ||
| c.pluginPath = initArgs.PluginPath | ||
| } | ||
|
|
||
| // Validate the arg count and get the working directory | ||
| path, err := ModulePath(initArgs.Args) | ||
| if err != nil { | ||
| diags = diags.Append(err) | ||
| view.Diagnostics(diags) | ||
| return 1 | ||
| } | ||
|
|
||
| if err := c.storePluginPath(c.pluginPath); err != nil { | ||
| diags = diags.Append(fmt.Errorf("Error saving -plugin-dir to workspace directory: %s", err)) | ||
| view.Diagnostics(diags) | ||
| return 1 | ||
| } | ||
|
|
||
| // Initialization can be aborted by interruption signals | ||
| 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 | ||
|
|
||
| empty, err := configs.IsEmptyDir(path, initArgs.TestsDirectory) | ||
| if err != nil { | ||
| diags = diags.Append(fmt.Errorf("Error validating destination directory: %s", err)) | ||
| view.Diagnostics(diags) | ||
| return 1 | ||
| } | ||
| if !empty { | ||
| diags = diags.Append(errors.New(strings.TrimSpace(errInitCopyNotEmpty))) | ||
| view.Diagnostics(diags) | ||
| return 1 | ||
| } | ||
|
|
||
| view.Output(views.CopyingConfigurationMessage, src) | ||
| header = true | ||
|
|
||
| hooks := uiModuleInstallHooks{ | ||
| Ui: c.Ui, | ||
| ShowLocalPaths: false, // since they are in a weird location for init | ||
| View: view, | ||
| } | ||
|
|
||
| ctx, span := tracer.Start(ctx, "-from-module=...", trace.WithAttributes( | ||
| attribute.String("module_source", src), | ||
| )) | ||
|
|
||
| initDirFromModuleAbort, initDirFromModuleDiags := c.initDirFromModule(ctx, path, src, hooks) | ||
| diags = diags.Append(initDirFromModuleDiags) | ||
| if initDirFromModuleAbort || initDirFromModuleDiags.HasErrors() { | ||
| view.Diagnostics(diags) | ||
| span.SetStatus(codes.Error, "module installation failed") | ||
| span.End() | ||
| return 1 | ||
| } | ||
| span.End() | ||
|
|
||
| view.Output(views.EmptyMessage) | ||
| } | ||
|
|
||
| // If our directory is empty, then we're done. We can't get or set up | ||
| // the backend with an empty directory. | ||
| empty, err := configs.IsEmptyDir(path, initArgs.TestsDirectory) | ||
| if err != nil { | ||
| diags = diags.Append(fmt.Errorf("Error checking configuration: %s", err)) | ||
| view.Diagnostics(diags) | ||
| return 1 | ||
| } | ||
| if empty { | ||
| view.Output(views.OutputInitEmptyMessage) | ||
| return 0 | ||
| } | ||
|
|
||
| // Load just the root module to begin backend and module initialization | ||
| rootModEarly, earlyConfDiags := c.loadSingleModuleWithTests(path, initArgs.TestsDirectory) | ||
|
|
||
| // There may be parsing errors in config loading but these will be shown later _after_ | ||
| // checking for core version requirement errors. Not meeting the version requirement should | ||
| // be the first error displayed if that is an issue, but other operations are required | ||
| // before being able to check core version requirements. | ||
| if rootModEarly == nil { | ||
| diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError)), earlyConfDiags) | ||
| view.Diagnostics(diags) | ||
|
|
||
| return 1 | ||
| } | ||
|
|
||
| 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) | ||
| // configDiags will be handled after the version constraint check, since an | ||
| // incorrect version of terraform may be producing 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 | ||
| } | ||
|
|
||
| // 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) | ||
|
|
||
| configProvidersOutput, configLocks, configProviderDiags := c.getProvidersFromConfig(ctx, config, initArgs.Upgrade, 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. | ||
| if header { | ||
| view.Output(views.EmptyMessage) | ||
| } | ||
|
|
||
| var back backend.Backend | ||
|
|
||
| var backDiags tfdiags.Diagnostics | ||
| var backendOutput bool | ||
| switch { | ||
| case initArgs.Cloud && rootModEarly.CloudConfig != nil: | ||
| back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view) | ||
| case initArgs.Backend: | ||
| // TODO(SarahFrench/radeksimko) - pass information about config locks (`configLocks`) into initBackend to | ||
| // enable PSS | ||
| back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view) | ||
| default: | ||
| // load the previously-stored backend config | ||
| 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) | ||
| } | ||
|
|
||
| var state *states.State | ||
|
|
||
| // If we have a functional backend (either just initialized or initialized | ||
| // on a previous run) we'll use the current state as a potential source | ||
| // of provider dependencies. | ||
| if back != nil { | ||
| c.ignoreRemoteVersionConflict(back) | ||
| workspace, err := c.Workspace() | ||
| if err != nil { | ||
| diags = diags.Append(fmt.Errorf("Error selecting workspace: %s", err)) | ||
| view.Diagnostics(diags) | ||
| return 1 | ||
| } | ||
| sMgr, err := back.StateMgr(workspace) | ||
| if err != nil { | ||
| diags = diags.Append(fmt.Errorf("Error loading state: %s", err)) | ||
| view.Diagnostics(diags) | ||
| return 1 | ||
| } | ||
|
|
||
| if err := sMgr.RefreshState(); err != nil { | ||
| diags = diags.Append(fmt.Errorf("Error refreshing state: %s", err)) | ||
| view.Diagnostics(diags) | ||
| return 1 | ||
| } | ||
|
|
||
| state = sMgr.State() | ||
| } | ||
|
|
||
| // Now the resource state is loaded, we can download the providers specified in the state but not the configuration. | ||
| // This is step two of a two-step provider download process | ||
| stateProvidersOutput, stateLocks, stateProvidersDiags := c.getProvidersFromState(ctx, state, configLocks, initArgs.Upgrade, initArgs.PluginPath, initArgs.Lockfile, view) | ||
| diags = diags.Append(configProviderDiags) | ||
| 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, configLocks, stateLocks, 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) | ||
| } | ||
|
|
||
| // As Terraform version-related diagnostics are handled above, we can now | ||
| // check the diagnostics from the early configuration and 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 | ||
| } | ||
|
|
||
| // Now, we can show any errors from initializing the backend, but we won't | ||
| // show the InitConfigError preamble as we didn't detect problems with | ||
| // the early configuration. | ||
| if backDiags.HasErrors() { | ||
| view.Diagnostics(diags) | ||
| return 1 | ||
| } | ||
|
|
||
| // If everything is ok with the core version check and backend 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 | ||
| } | ||
|
|
||
| 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. | ||
| view.Diagnostics(diags) | ||
| _, cloud := back.(*cloud.Cloud) | ||
| output := views.OutputInitSuccessMessage | ||
| if cloud { | ||
| output = views.OutputInitSuccessCloudMessage | ||
| } | ||
|
|
||
| view.Output(output) | ||
|
|
||
| if !c.RunningInAutomation { | ||
| // If we're not running in an automation wrapper, give the user | ||
| // some more detailed next steps that are appropriate for interactive | ||
| // shell usage. | ||
| output = views.OutputInitSuccessCLIMessage | ||
| if cloud { | ||
| output = views.OutputInitSuccessCLICloudMessage | ||
| } | ||
| view.Output(output) | ||
| } | ||
| return 0 | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm half mind as to whether we should be more open here and say in the
Detailpart that it's experimental and suggest them to use experimental version 🤔 maybe not.