Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
229f42f
Add a generic method for loading an operations backend in non-init co…
SarahFrench Sep 8, 2025
082f481
Refactor commands to use new prepareBackend method: group 1
SarahFrench Sep 8, 2025
70c4f39
Refactor commands to use new prepareBackend method: group 2, where co…
SarahFrench Sep 8, 2025
44b9e4a
Refactor commands to use new prepareBackend method: group 3, where we…
SarahFrench Sep 8, 2025
5bacdce
Additional, more nested, places where logic for accessing backends ne…
SarahFrench Sep 8, 2025
7d5f88d
Remove duplicated comment
SarahFrench Sep 8, 2025
323b179
Add test coverage of `(m *Meta) prepareBackend()`
SarahFrench Sep 9, 2025
1afde44
Add TODO related to using plans for backend/state_store config in app…
SarahFrench Oct 20, 2025
5ba4a6f
Add `testStateStoreMockWithChunkNegotiation` test helper
SarahFrench Oct 20, 2025
ae6db5c
Add assertions to tests about the backend (remote-state, local, etc) …
SarahFrench Oct 20, 2025
be43ec6
Stop prepareBackend taking locks as argument
SarahFrench Oct 20, 2025
823c5b4
Code comment in prepareBackend
SarahFrench Oct 20, 2025
96269e9
Replace c.Meta.prepareBackend with c.prepareBackend
SarahFrench Oct 22, 2025
b71931c
Change `c.Meta.loadSingleModule` to `c.loadSingleModule`
SarahFrench Oct 22, 2025
9ca9710
Rename (Meta).prepareBackend to (Meta).backend, update godoc comment…
SarahFrench Oct 22, 2025
c8b001c
Revert change from config.Module to config.Root.Module
SarahFrench Oct 22, 2025
6698109
Update `(m *Meta) backend` method to parse config itself, and also to…
SarahFrench Oct 24, 2025
84fb3f0
Update all tests and calling code following previous commit
SarahFrench Oct 24, 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
16 changes: 6 additions & 10 deletions internal/command/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,19 +219,15 @@ func (c *ApplyCommand) PrepareBackend(planFile *planfile.WrappedPlanFile, args *
))
return nil, diags
}
// TODO: Update BackendForLocalPlan to use state storage, and plan to be able to contain State Store config details
be, beDiags = c.BackendForLocalPlan(plan.Backend)
Comment on lines 221 to 223
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is scoped to this ticket: https://hashicorp.atlassian.net/browse/TF-28374
I've linked here from that ticket.

} else {
// Both new plans and saved cloud plans load their backend from config.
backendConfig, configDiags := c.loadBackendConfig(".")
diags = diags.Append(configDiags)
if configDiags.HasErrors() {
return nil, diags
}

be, beDiags = c.Backend(&BackendOpts{
BackendConfig: backendConfig,
ViewType: viewType,
})
// Load the backend
//
// Note: Both new plans and saved cloud plans load their backend from config,
// hence the config parsing in the method below.
be, beDiags = c.backend(".", viewType)
}

diags = diags.Append(beDiags)
Expand Down
11 changes: 1 addition & 10 deletions internal/command/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,8 @@ func (c *ConsoleCommand) Run(args []string) int {

var diags tfdiags.Diagnostics

backendConfig, backendDiags := c.loadBackendConfig(configPath)
diags = diags.Append(backendDiags)
if diags.HasErrors() {
c.showDiagnostics(diags)
return 1
}

// Load the backend
b, backendDiags := c.Backend(&BackendOpts{
BackendConfig: backendConfig,
})
b, backendDiags := c.backend(configPath, arguments.ViewHuman)
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)
Expand Down
11 changes: 1 addition & 10 deletions internal/command/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,8 @@ func (c *GraphCommand) Run(args []string) int {

var diags tfdiags.Diagnostics

backendConfig, backendDiags := c.loadBackendConfig(configPath)
diags = diags.Append(backendDiags)
if diags.HasErrors() {
c.showDiagnostics(diags)
return 1
}

// Load the backend
b, backendDiags := c.Backend(&BackendOpts{
BackendConfig: backendConfig,
})
b, backendDiags := c.backend(".", arguments.ViewHuman)
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)
Expand Down
4 changes: 1 addition & 3 deletions internal/command/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,7 @@ func (c *ImportCommand) Run(args []string) int {
}

// Load the backend
b, backendDiags := c.Backend(&BackendOpts{
BackendConfig: config.Module.Backend,
})
b, backendDiags := c.backend(".", arguments.ViewHuman)
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)
Expand Down
76 changes: 76 additions & 0 deletions internal/command/meta_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -1580,6 +1580,82 @@ func (m *Meta) updateSavedBackendHash(cHash int, sMgr *clistate.LocalState) tfdi
return diags
}

// backend returns an operations backend that may use a backend, cloud, or state_store block for state storage.
// Based on the supplied config, it prepares arguments to pass into (Meta).Backend, which returns the operations backend.
//
// This method should be used in NON-init operations only; it's incapable of processing new init command CLI flags used
// for partial configuration, however it will use the backend state file to use partial configuration from a previous
// init command.
func (m *Meta) backend(configPath string, viewType arguments.ViewType) (backendrun.OperationsBackend, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics

if configPath == "" {
configPath = "."
}

// Only return error diagnostics at this point. Any warnings will be caught
// again later and duplicated in the output.
root, mDiags := m.loadSingleModule(configPath)
if mDiags.HasErrors() {
diags = diags.Append(mDiags)
return nil, diags
}

var opts *BackendOpts
switch {
case root.Backend != nil:
opts = &BackendOpts{
BackendConfig: root.Backend,
ViewType: viewType,
}
case root.CloudConfig != nil:
backendConfig := root.CloudConfig.ToBackendConfig()
opts = &BackendOpts{
BackendConfig: &backendConfig,
ViewType: viewType,
}
case root.StateStore != nil:
// In addition to config, use of a state_store requires
// provider factory and provider locks data
locks, lDiags := m.lockedDependencies()
diags = diags.Append(lDiags)
if lDiags.HasErrors() {
return nil, diags
}

factory, fDiags := m.GetStateStoreProviderFactory(root.StateStore, locks)
diags = diags.Append(fDiags)
if fDiags.HasErrors() {
return nil, diags
}

opts = &BackendOpts{
StateStoreConfig: root.StateStore,
ProviderFactory: factory,
Locks: locks,
ViewType: viewType,
}
default:
// there is no config; defaults to local state storage
opts = &BackendOpts{
ViewType: viewType,
}
}

// This method should not be used for init commands,
// so we always set this value as false.
opts.Init = false

// Load the backend
be, beDiags := m.Backend(opts)
diags = diags.Append(beDiags)
if beDiags.HasErrors() {
return nil, diags
}

return be, diags
}

//-------------------------------------------------------------------
// State Store Config Scenarios
// The functions below cover handling all the various scenarios that
Expand Down
136 changes: 136 additions & 0 deletions internal/command/meta_backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"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/clistate"
"github.com/hashicorp/terraform/internal/command/workdir"
"github.com/hashicorp/terraform/internal/configs"
Expand Down Expand Up @@ -2962,6 +2963,126 @@ func Test_getStateStorageProviderVersion(t *testing.T) {
})
}

func TestMetaBackend_prepareBackend(t *testing.T) {

t.Run("it returns a cloud backend from cloud backend config", func(t *testing.T) {
// Create a temporary working directory with cloud configuration in
td := t.TempDir()
testCopyDir(t, testFixturePath("cloud-config"), td)
t.Chdir(td)

m := testMetaBackend(t, nil)

// We cannot initialize a cloud backend so we instead check
// the init error is referencing HCP Terraform
_, bDiags := m.backend(td, arguments.ViewHuman)
if !bDiags.HasErrors() {
t.Fatal("expected error but got none")
}
wantErr := "HCP Terraform or Terraform Enterprise initialization required: please run \"terraform init\""
if !strings.Contains(bDiags.Err().Error(), wantErr) {
t.Fatalf("expected error to contain %q, but got: %q",
wantErr,
bDiags.Err())
}
})

t.Run("it returns a backend from backend config", func(t *testing.T) {
// Create a temporary working directory with backend configuration in
td := t.TempDir()
testCopyDir(t, testFixturePath("backend-unchanged"), td)
t.Chdir(td)

m := testMetaBackend(t, nil)

b, bDiags := m.backend(td, arguments.ViewHuman)
if bDiags.HasErrors() {
t.Fatal("unexpected error: ", bDiags.Err())
}

if _, ok := b.(*local.Local); !ok {
t.Fatal("expected returned operations backend to be a Local backend")
}
// Check the type of backend inside the Local via schema
// In this case a `local` backend should have been returned by default.
//
// Look for the path attribute.
schema := b.ConfigSchema()
if _, ok := schema.Attributes["path"]; !ok {
t.Fatalf("expected the operations backend to report the schema of a local backend, but got something unexpected: %#v", schema)
}
})

t.Run("it returns a local backend when there is empty configuration", func(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("empty"), td)
t.Chdir(td)

m := testMetaBackend(t, nil)
b, bDiags := m.backend(td, arguments.ViewHuman)
if bDiags.HasErrors() {
t.Fatal("unexpected error: ", bDiags.Err())
}

if _, ok := b.(*local.Local); !ok {
t.Fatal("expected returned operations backend to be a Local backend")
}
// Check the type of backend inside the Local via schema
// In this case a `local` backend should have been returned by default.
//
// Look for the path attribute.
schema := b.ConfigSchema()
if _, ok := schema.Attributes["path"]; !ok {
t.Fatalf("expected the operations backend to report the schema of a local backend, but got something unexpected: %#v", schema)
}
})

t.Run("it returns a state_store from state_store config", func(t *testing.T) {
// Create a temporary working directory with backend configuration in
td := t.TempDir()
testCopyDir(t, testFixturePath("state-store-unchanged"), td)
t.Chdir(td)

m := testMetaBackend(t, nil)
m.AllowExperimentalFeatures = true
mock := testStateStoreMockWithChunkNegotiation(t, 12345) // chunk size needs to be set, value is arbitrary
m.testingOverrides = &testingOverrides{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): providers.FactoryFixed(mock),
},
}

// Prepare appropriate locks; config uses a hashicorp/test provider @ v1.2.3
locks := depsfile.NewLocks()
providerAddr := addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/test")
constraint, err := providerreqs.ParseVersionConstraints(">1.0.0")
if err != nil {
t.Fatalf("test setup failed when making constraint: %s", err)
}
locks.SetProvider(
providerAddr,
versions.MustParseVersion("1.2.3"),
constraint,
[]providerreqs.Hash{""},
)

b, bDiags := m.backend(td, arguments.ViewHuman)
if bDiags.HasErrors() {
t.Fatalf("unexpected error: %s", bDiags.Err())
}

if _, ok := b.(*local.Local); !ok {
t.Fatal("expected returned operations backend to be a Local backend")
}
// Check the state_store inside the Local via schema
// Look for the mock state_store's attribute called `value`.
schema := b.ConfigSchema()
if _, ok := schema.Attributes["value"]; !ok {
t.Fatalf("expected the operations backend to report the schema of the state_store, but got something unexpected: %#v", schema)
}
})
}

func testMetaBackend(t *testing.T, args []string) *Meta {
var m Meta
m.Ui = new(cli.MockUi)
Expand Down Expand Up @@ -3011,6 +3132,21 @@ func testStateStoreMock(t *testing.T) *testing_provider.MockProvider {
}
}

// testStateStoreMockWithChunkNegotiation is just like testStateStoreMock but the returned mock is set up so it'll be configured
// without this error: `Failed to negotiate acceptable chunk size`
//
// This is meant to be a convenience method when a test is definitely not testing anything related to state store configuration.
func testStateStoreMockWithChunkNegotiation(t *testing.T, chunkSize int64) *testing_provider.MockProvider {
t.Helper()
mock := testStateStoreMock(t)
mock.ConfigureStateStoreResponse = &providers.ConfigureStateStoreResponse{
Capabilities: providers.StateStoreServerCapabilities{
ChunkSize: chunkSize,
},
}
return mock
}

func configBodyForTest(t *testing.T, config string) hcl.Body {
t.Helper()
f, diags := hclsyntax.ParseConfig([]byte(config), "", hcl.Pos{Line: 1, Column: 1})
Expand Down
4 changes: 2 additions & 2 deletions internal/command/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ func (c *OutputCommand) Outputs(statePath string) (map[string]*states.OutputValu
}

// Load the backend
b, backendDiags := c.Backend(nil)
b, backendDiags := c.backend(".", arguments.ViewHuman)
diags = diags.Append(backendDiags)
if diags.HasErrors() {
if backendDiags.HasErrors() {
return nil, diags
}

Expand Down
13 changes: 2 additions & 11 deletions internal/command/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,18 +124,9 @@ func (c *PlanCommand) PrepareBackend(args *arguments.State, viewType arguments.V
// difficult but would make their use easier to understand.
c.Meta.applyStateArguments(args)

backendConfig, diags := c.loadBackendConfig(".")
if diags.HasErrors() {
return nil, diags
}

// Load the backend
be, beDiags := c.Backend(&BackendOpts{
BackendConfig: backendConfig,
ViewType: viewType,
})
diags = diags.Append(beDiags)
if beDiags.HasErrors() {
be, diags := c.backend(".", arguments.ViewHuman)
if diags.HasErrors() {
return nil, diags
}

Expand Down
5 changes: 2 additions & 3 deletions internal/command/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/xlab/treeprint"

"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/internal/tfdiags"
Expand Down Expand Up @@ -81,9 +82,7 @@ func (c *ProvidersCommand) Run(args []string) int {
}

// Load the backend
b, backendDiags := c.Backend(&BackendOpts{
BackendConfig: config.Module.Backend,
})
b, backendDiags := c.backend(".", arguments.ViewHuman)
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)
Expand Down
2 changes: 1 addition & 1 deletion internal/command/providers_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (c *ProvidersSchemaCommand) Run(args []string) int {
var diags tfdiags.Diagnostics

// Load the backend
b, backendDiags := c.Backend(nil)
b, backendDiags := c.backend(".", arguments.ViewHuman)
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)
Expand Down
15 changes: 1 addition & 14 deletions internal/command/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,21 +154,8 @@ func (c *QueryCommand) Run(rawArgs []string) int {
}

func (c *QueryCommand) PrepareBackend(args *arguments.State, viewType arguments.ViewType) (backendrun.OperationsBackend, tfdiags.Diagnostics) {
backendConfig, diags := c.loadBackendConfig(".")
if diags.HasErrors() {
return nil, diags
}

// Load the backend
be, beDiags := c.Backend(&BackendOpts{
BackendConfig: backendConfig,
ViewType: viewType,
})
diags = diags.Append(beDiags)
if beDiags.HasErrors() {
return nil, diags
}

be, diags := c.backend(".", viewType)
return be, diags
}

Expand Down
Loading