Skip to content
Merged
Show file tree
Hide file tree
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 Jul 15, 2025
58b4ee7
Reorder actions in experimental init - load in full config before con…
SarahFrench Jul 16, 2025
a214b41
Add getProvidersFromConfig method, initially as an exact copy of getP…
SarahFrench Jul 15, 2025
37352b3
Make getProvidersFromConfig not use state to get providers
SarahFrench Jul 15, 2025
143c6d0
Add `appendLockedDependencies` method to `Meta` to allow multi-phase …
SarahFrench Jul 16, 2025
0875016
Update experimental init to use new getProvidersFromConfig method
SarahFrench Jul 16, 2025
c41ac8c
Add new getProvidersFromState method that only accepts state informat…
SarahFrench Jul 16, 2025
4cacf15
Update messages sent to view about provider download phases
SarahFrench Jul 17, 2025
a85eee9
Change init to save updates to the deps lock file only once
SarahFrench Jul 17, 2025
90bb924
Make Terraform output report that a lock file _will_ be made after pr…
SarahFrench Jul 17, 2025
4714764
Remove use of `ProviderDownloadOutcome`s
SarahFrench Jul 31, 2025
07cf87c
Move repeated code into separate method
SarahFrench Jul 31, 2025
f74c4f0
Change provider download approach: determine if locks changed at poin…
SarahFrench Aug 1, 2025
98c4677
Refactor `mergeLockedDependencies` and update test
SarahFrench Aug 1, 2025
952e14c
Add comments to provider download methods
SarahFrench Aug 1, 2025
f40917b
Fix issue where incorrect message ouput to view when downloading prov…
SarahFrench Aug 4, 2025
20f2491
Update `mergeLockedDependencies` method to be more generic
SarahFrench Aug 4, 2025
4ea7c2b
Update `getProvidersFromState` method to receive in-progress config l…
SarahFrench Aug 4, 2025
dcf6d39
Fix config for `TestInit_stateStoreBlockIsExperimental`
SarahFrench Aug 4, 2025
5556043
Improve testing of mergeLockedDependencies; state locks are always mi…
SarahFrench Aug 5, 2025
3889abe
Add tests for 2 phase provider download
SarahFrench Aug 4, 2025
b05865e
Add test case to cover use of the `-upgrade` flag
SarahFrench Aug 6, 2025
9c948e5
Change the message shown when a provider is reused during the second …
SarahFrench Aug 6, 2025
88cfedb
Update mergeLockedDependencies comment
SarahFrench Aug 6, 2025
3129ef4
fix: completely remove use of upgrade flag in getProvidersFromState
SarahFrench Aug 8, 2025
2665767
Fix: avoid nil pointer errors by returning an empty collection of loc…
SarahFrench Aug 8, 2025
03adc1d
Fix: use state store data only in diagnostic
SarahFrench Aug 8, 2025
63a9b92
Change how we make PSS experimental - avoid relying on a package leve…
SarahFrench Aug 8, 2025
1ea29ca
Remove full-stop in view message, update tests
SarahFrench Aug 12, 2025
c82186b
Update span names to be unique
SarahFrench Aug 14, 2025
14ba3e7
Re-add lost early returns
SarahFrench Aug 14, 2025
da3e372
Remove unused view messages
SarahFrench Aug 14, 2025
31f2cc1
Add comments to new view messages
SarahFrench Aug 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/command/cloud_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func TestCloud_withBackendConfig(t *testing.T) {

// Initialize the backend
ic := &InitCommand{
Meta{
Meta: Meta{
Ui: ui,
View: view,
testingOverrides: metaOverridesForProvider(testProvider()),
Expand Down
586 changes: 582 additions & 4 deletions internal/command/init.go

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions internal/command/init_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"strings"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/cloud"
"github.com/hashicorp/terraform/internal/command/arguments"
Expand Down Expand Up @@ -141,6 +142,22 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int {

return 1
}
if !c.Meta.AllowExperimentalFeatures && rootModEarly.StateStore != nil {
// TODO(SarahFrench/radeksimko) - remove when this feature isn't experimental.
// This approach for making the feature experimental is required
// to let us assert the feature is gated behind an experiment in tests.
// See https://github.com/hashicorp/terraform/pull/37350#issuecomment-3168555619
diags = diags.Append(earlyConfDiags)
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unsupported block type",
Detail: "Blocks of type \"state_store\" are not expected here.",
Subject: &rootModEarly.StateStore.TypeRange,
})
Comment on lines +151 to +156
Copy link
Copy Markdown
Member

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 Detail part that it's experimental and suggest them to use experimental version 🤔 maybe not.

view.Diagnostics(diags)

return 1
}

var back backend.Backend

Expand Down
346 changes: 346 additions & 0 deletions internal/command/init_run_experiment.go
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
}
Loading