Skip to content
Open
Show file tree
Hide file tree
Changes from 12 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
13 changes: 6 additions & 7 deletions internal/command/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,19 +219,18 @@ 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() {
mod, mDiags := c.Meta.loadSingleModule(".")
if mDiags.HasErrors() {
diags = diags.Append(mDiags)
return nil, diags
}

be, beDiags = c.Backend(&BackendOpts{
BackendConfig: backendConfig,
ViewType: viewType,
})
// Load the backend
be, beDiags = c.Meta.prepareBackend(mod)
}

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

var diags tfdiags.Diagnostics

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

// Load the backend
b, backendDiags := c.Backend(&BackendOpts{
BackendConfig: backendConfig,
})
b, backendDiags := c.Meta.prepareBackend(mod)
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)
Expand Down
7 changes: 2 additions & 5 deletions internal/command/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,14 @@ func (c *GraphCommand) Run(args []string) int {

var diags tfdiags.Diagnostics

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

// Load the backend
b, backendDiags := c.Backend(&BackendOpts{
BackendConfig: backendConfig,
})
b, backendDiags := c.Meta.prepareBackend(mod)
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.Meta.prepareBackend(config.Root.Module)
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)
Expand Down
57 changes: 57 additions & 0 deletions internal/command/meta_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -1580,6 +1580,63 @@ func (m *Meta) updateSavedBackendHash(cHash int, sMgr *clistate.LocalState) tfdi
return diags
}

// prepareBackend returns an operations backend that may use a backend, cloud, or state_store block for state storage.
// 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) prepareBackend(root *configs.Module) (backendrun.OperationsBackend, tfdiags.Diagnostics) {
Copy link
Member

Choose a reason for hiding this comment

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

Mostly readability related nitpick: I'd consider making this method's arguments a bit less "greedy", e.g. backend, cloudConfig, stateStore.

Then it makes it more obvious to the reader from all the calling places that this method cannot possibly do any business with the rest of the module.

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 could resolve this by making this method load the root module, instead of using data from the calling code.

That would address this concern about duplicated warnings and resembles the original backendConfig method used.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah but now I take a proper look I realise that one of the things I was trying to do was avoid the root module being parsed multiple times (e.g. in internal/command/providers.go the diff uses some already-parsed config as the argument to this method).

I'll think on it more.

Copy link
Member Author

@SarahFrench SarahFrench Oct 30, 2025

Choose a reason for hiding this comment

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

var diags tfdiags.Diagnostics

var opts *BackendOpts
switch {
case root.Backend != nil:
opts = &BackendOpts{
BackendConfig: root.Backend,
}
case root.CloudConfig != nil:
backendConfig := root.CloudConfig.ToBackendConfig()
opts = &BackendOpts{
BackendConfig: &backendConfig,
}
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,
}
default:
// there is no config; defaults to local state storage
opts = &BackendOpts{}
}

// 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
151 changes: 151 additions & 0 deletions internal/command/meta_backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2962,6 +2962,142 @@ 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)

// Get the cloud config
mod, loadDiags := m.loadSingleModule(td)
if loadDiags.HasErrors() {
t.Fatalf("unexpected error when loading test config: %s", loadDiags.Err())
}

// We cannot initialize a cloud backend so we instead check
// the init error is referencing HCP Terraform
_, bDiags := m.prepareBackend(mod)
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)

// Get the backend config
mod, loadDiags := m.loadSingleModule(td)
if loadDiags.HasErrors() {
t.Fatalf("unexpected error when loading test config: %s", loadDiags.Err())
}

b, bDiags := m.prepareBackend(mod)
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) {
m := testMetaBackend(t, nil)
emptyConfig := configs.NewEmptyConfig()

b, bDiags := m.prepareBackend(emptyConfig.Module)
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),
},
}

// Get the backend config
mod, loadDiags := m.loadSingleModule(td)
if loadDiags.HasErrors() {
t.Fatalf("unexpected error when loading test config: %s", loadDiags.Err())
}

// 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.prepareBackend(mod)
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 +3147,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
9 changes: 7 additions & 2 deletions internal/command/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,15 @@ func (c *OutputCommand) Outputs(statePath string) (map[string]*states.OutputValu
c.Meta.statePath = statePath
}

mod, diags := c.Meta.loadSingleModule(".")
if diags.HasErrors() {
return nil, diags
}

// Load the backend
b, backendDiags := c.Backend(nil)
b, backendDiags := c.Meta.prepareBackend(mod)
diags = diags.Append(backendDiags)
if diags.HasErrors() {
if backendDiags.HasErrors() {
return nil, diags
}

Expand Down
7 changes: 2 additions & 5 deletions internal/command/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,13 @@ 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(".")
mod, diags := c.Meta.loadSingleModule(".")
if diags.HasErrors() {
return nil, diags
}

// Load the backend
be, beDiags := c.Backend(&BackendOpts{
BackendConfig: backendConfig,
ViewType: viewType,
})
be, beDiags := c.Meta.prepareBackend(mod)
diags = diags.Append(beDiags)
if beDiags.HasErrors() {
return nil, diags
Expand Down
4 changes: 1 addition & 3 deletions internal/command/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,7 @@ func (c *ProvidersCommand) Run(args []string) int {
}

// Load the backend
b, backendDiags := c.Backend(&BackendOpts{
BackendConfig: config.Module.Backend,
})
b, backendDiags := c.Meta.prepareBackend(config.Root.Module)
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)
Expand Down
8 changes: 7 additions & 1 deletion internal/command/providers_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,14 @@ func (c *ProvidersSchemaCommand) Run(args []string) int {

var diags tfdiags.Diagnostics

mod, diags := c.Meta.loadSingleModule(".")
if diags.HasErrors() {
c.showDiagnostics(diags)
return 1
}

// Load the backend
b, backendDiags := c.Backend(nil)
b, backendDiags := c.Meta.prepareBackend(mod)
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)
Expand Down
11 changes: 6 additions & 5 deletions internal/command/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,16 +154,17 @@ 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(".")
mod, diags := c.Meta.loadSingleModule(".")
if diags.HasErrors() {
return nil, diags
}

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