Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 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
733618f
Change how an operations backend is obtained by autocomplete code
SarahFrench Oct 30, 2025
7497796
Update autocomplete to return nil if no workspace names are returned …
SarahFrench Oct 30, 2025
0cf0769
Add test coverage for autocompleting workspace names when using a plu…
SarahFrench Oct 31, 2025
b07a903
Fix output command: pass view type data to new `backend` method
SarahFrench Oct 31, 2025
99fc261
Fix in plan command: pass correct view type to `backend` method
SarahFrench Oct 31, 2025
e9b15ec
Fix `providers schema` command to use correct viewtype when preparing…
SarahFrench Oct 31, 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
14 changes: 7 additions & 7 deletions internal/command/autocomplete.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package command

import (
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/posener/complete"
)

Expand Down Expand Up @@ -48,19 +49,18 @@ func (m *Meta) completePredictWorkspaceName() complete.Predictor {
return nil
}

backendConfig, diags := m.loadBackendConfig(configPath)
b, diags := m.backend(configPath, arguments.ViewHuman)
if diags.HasErrors() {
return nil
}

b, diags := m.Backend(&BackendOpts{
BackendConfig: backendConfig,
})
if diags.HasErrors() {
names, _ := b.Workspaces()
if len(names) == 0 {
// Presence of the "default" isn't always guaranteed
// Backends will report it as always existing, pluggable
// state stores will only do so if it _actually_ exists.
return nil
}

names, _ := b.Workspaces()
return names
})
}
127 changes: 107 additions & 20 deletions internal/command/autocomplete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,124 @@
package command

import (
"io/ioutil"
"os"
"reflect"
"testing"

"github.com/hashicorp/cli"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/command/workdir"
"github.com/hashicorp/terraform/internal/providers"
"github.com/posener/complete"
)

func TestMetaCompletePredictWorkspaceName(t *testing.T) {
// Create a temporary working directory that is empty
td := t.TempDir()
os.MkdirAll(td, 0755)
t.Chdir(td)

// make sure a vars file doesn't interfere
err := ioutil.WriteFile(DefaultVarsFilename, nil, 0644)
if err != nil {
t.Fatal(err)
}
t.Run("test autocompletion using the local backend", func(t *testing.T) {
// Create a temporary working directory that is empty
td := t.TempDir()
t.Chdir(td)

ui := new(cli.MockUi)
meta := &Meta{Ui: ui}
ui := new(cli.MockUi)
meta := &Meta{Ui: ui}

predictor := meta.completePredictWorkspaceName()
predictor := meta.completePredictWorkspaceName()

got := predictor.Predict(complete.Args{
Last: "",
got := predictor.Predict(complete.Args{
Last: "",
})
want := []string{"default"}
if !reflect.DeepEqual(got, want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want)
}
})

t.Run("test autocompletion using a state store", func(t *testing.T) {
// Create a temporary working directory with state_store config
td := t.TempDir()
testCopyDir(t, testFixturePath("state-store-unchanged"), td)
t.Chdir(td)

// Set up pluggable state store provider mock
mockProvider := mockPluggableStateStorageProvider()
// Mock the existence of workspaces
mockProvider.MockStates = map[string]interface{}{
"default": true,
"foobar": true,
}
mockProviderAddress := addrs.NewDefaultProvider("test")
providerSource, close := newMockProviderSource(t, map[string][]string{
"hashicorp/test": {"1.0.0"},
})
defer close()

ui := new(cli.MockUi)
view, _ := testView(t)
wd := workdir.NewDir(".")
wd.OverrideOriginalWorkingDir(td)
meta := Meta{
WorkingDir: wd, // Use the test's temp dir
Ui: ui,
View: view,
AllowExperimentalFeatures: true,
testingOverrides: &testingOverrides{
Providers: map[addrs.Provider]providers.Factory{
mockProviderAddress: providers.FactoryFixed(mockProvider),
},
},
ProviderSource: providerSource,
}

predictor := meta.completePredictWorkspaceName()

got := predictor.Predict(complete.Args{
Last: "",
})
want := []string{"default", "foobar"}
if !reflect.DeepEqual(got, want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want)
}
})

t.Run("test autocompletion using a state store containing no workspaces", func(t *testing.T) {
// Create a temporary working directory with state_store config
td := t.TempDir()
testCopyDir(t, testFixturePath("state-store-unchanged"), td)
t.Chdir(td)

// Set up pluggable state store provider mock
mockProvider := mockPluggableStateStorageProvider()
// No workspaces exist in the mock
mockProvider.MockStates = map[string]interface{}{}
mockProviderAddress := addrs.NewDefaultProvider("test")
providerSource, close := newMockProviderSource(t, map[string][]string{
"hashicorp/test": {"1.0.0"},
})
defer close()

ui := new(cli.MockUi)
view, _ := testView(t)
wd := workdir.NewDir(".")
wd.OverrideOriginalWorkingDir(td)
meta := Meta{
WorkingDir: wd, // Use the test's temp dir
Ui: ui,
View: view,
AllowExperimentalFeatures: true,
testingOverrides: &testingOverrides{
Providers: map[addrs.Provider]providers.Factory{
mockProviderAddress: providers.FactoryFixed(mockProvider),
},
},
ProviderSource: providerSource,
}

predictor := meta.completePredictWorkspaceName()

got := predictor.Predict(complete.Args{
Last: "",
})
if got != nil {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, nil)
}
})
want := []string{"default"}
if !reflect.DeepEqual(got, want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want)
}
}
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)
Copy link
Member

@radeksimko radeksimko Oct 31, 2025

Choose a reason for hiding this comment

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

I know this isn't related to your PR really but it's interesting to note the graph command output is always "machine-readable" in the sense that it's DOT language which a human would first pipe into something like graphviz before they can read it. 😅

Though it's not JSON, so neither of the two view types we have are a good match. 🤷🏻

And yes - mimicking the existing default behaviour is a very sensible approach!

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
}
Comment on lines +1596 to +1602
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've made the new backend method resume being the code to parse the configuration for backend/state_store config. This means that some commands parse the configuration twice, as this code is no longer able to accept parsed config as input.

Copy link
Member

Choose a reason for hiding this comment

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

Ack, I expect this will have some performance impact but also I hope/assume it will be negligible in the grand scheme of things.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah agreed - I think only a few commands would be negatively affected so it's not too bad


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
Loading