From 229f42f4c4d7271c7c18d4ed93adbcfb8a7c92f0 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 8 Sep 2025 10:52:54 +0100 Subject: [PATCH 01/12] Add a generic method for loading an operations backend in non-init commands --- internal/command/meta_backend.go | 56 ++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index 46241932b09f..e809905aac20 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -1580,6 +1580,62 @@ 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) { + 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 + factory, fDiags := m.getStateStoreProviderFactory(root.StateStore) + diags = diags.Append(fDiags) + if fDiags.HasErrors() { + return nil, diags + } + + // TODO - Use locks from here in opts below + _, lDiags := m.lockedDependencies() + diags = diags.Append(lDiags) + if lDiags.HasErrors() { + return nil, diags + } + + opts = &BackendOpts{ + StateStoreConfig: root.StateStore, + ProviderFactory: factory, + // TODO - update once other work is merged into main + // Locks: locks, + } + default: + // there is no config; defaults to local state storage + opts = &BackendOpts{} + } + opts.Init = false // To be explicit- this method should not be used in init commands! + + // 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 From 082f4816ceadccfafd5027c7df3902b2bb788d50 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 8 Sep 2025 12:18:18 +0100 Subject: [PATCH 02/12] Refactor commands to use new prepareBackend method: group 1 --- internal/command/apply.go | 12 +++++------- internal/command/console.go | 7 ++----- internal/command/graph.go | 7 ++----- internal/command/plan.go | 7 ++----- internal/command/query.go | 11 ++++++----- internal/command/refresh.go | 7 ++----- internal/command/state_list.go | 13 +++++++++---- internal/command/unlock.go | 7 ++----- internal/command/workspace_delete.go | 7 ++----- internal/command/workspace_list.go | 7 ++----- internal/command/workspace_new.go | 7 ++----- internal/command/workspace_select.go | 8 ++------ 12 files changed, 38 insertions(+), 62 deletions(-) diff --git a/internal/command/apply.go b/internal/command/apply.go index cd0b1e958ad4..23722c1baf87 100644 --- a/internal/command/apply.go +++ b/internal/command/apply.go @@ -222,16 +222,14 @@ func (c *ApplyCommand) PrepareBackend(planFile *planfile.WrappedPlanFile, args * be, beDiags = c.BackendForLocalPlan(plan.Backend) } 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) diff --git a/internal/command/console.go b/internal/command/console.go index a66b959d89a9..0a37936ca665 100644 --- a/internal/command/console.go +++ b/internal/command/console.go @@ -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) diff --git a/internal/command/graph.go b/internal/command/graph.go index 538438d772b8..b794f950dd0d 100644 --- a/internal/command/graph.go +++ b/internal/command/graph.go @@ -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) diff --git a/internal/command/plan.go b/internal/command/plan.go index a6f0b1400181..79559b28234a 100644 --- a/internal/command/plan.go +++ b/internal/command/plan.go @@ -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 diff --git a/internal/command/query.go b/internal/command/query.go index 5faf2f0616d8..54081ef456cc 100644 --- a/internal/command/query.go +++ b/internal/command/query.go @@ -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 diff --git a/internal/command/refresh.go b/internal/command/refresh.go index 970e2a6167be..dc2465898c20 100644 --- a/internal/command/refresh.go +++ b/internal/command/refresh.go @@ -117,16 +117,13 @@ func (c *RefreshCommand) PrepareBackend(args *arguments.State, viewType argument // 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 diff --git a/internal/command/state_list.go b/internal/command/state_list.go index dd211ebfe383..51ebc92daf72 100644 --- a/internal/command/state_list.go +++ b/internal/command/state_list.go @@ -10,7 +10,6 @@ import ( "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/states" - "github.com/hashicorp/terraform/internal/tfdiags" ) // StateListCommand is a Command implementation that lists the resources @@ -36,10 +35,17 @@ func (c *StateListCommand) Run(args []string) int { c.Meta.statePath = statePath } + 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(backendDiags) + c.showDiagnostics(diags) return 1 } @@ -69,7 +75,6 @@ func (c *StateListCommand) Run(args []string) int { } var addrs []addrs.AbsResourceInstance - var diags tfdiags.Diagnostics if len(args) == 0 { addrs, diags = c.lookupAllResourceInstanceAddrs(state) } else { diff --git a/internal/command/unlock.go b/internal/command/unlock.go index b66e13050157..063df1102b5a 100644 --- a/internal/command/unlock.go +++ b/internal/command/unlock.go @@ -51,17 +51,14 @@ func (c *UnlockCommand) 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) diff --git a/internal/command/workspace_delete.go b/internal/command/workspace_delete.go index 8213014579c5..d541cf36e7f3 100644 --- a/internal/command/workspace_delete.go +++ b/internal/command/workspace_delete.go @@ -59,17 +59,14 @@ func (c *WorkspaceDeleteCommand) 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) diff --git a/internal/command/workspace_list.go b/internal/command/workspace_list.go index 17e85938dceb..a4f1e7556e38 100644 --- a/internal/command/workspace_list.go +++ b/internal/command/workspace_list.go @@ -37,17 +37,14 @@ func (c *WorkspaceListCommand) 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) diff --git a/internal/command/workspace_new.go b/internal/command/workspace_new.go index b0cd57007640..ba630be40137 100644 --- a/internal/command/workspace_new.go +++ b/internal/command/workspace_new.go @@ -68,17 +68,14 @@ func (c *WorkspaceNewCommand) 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) diff --git a/internal/command/workspace_select.go b/internal/command/workspace_select.go index 0696fa677b3a..a3e1189aa65b 100644 --- a/internal/command/workspace_select.go +++ b/internal/command/workspace_select.go @@ -43,9 +43,7 @@ func (c *WorkspaceSelectCommand) 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 @@ -58,9 +56,7 @@ func (c *WorkspaceSelectCommand) Run(args []string) int { } // 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) From 70c4f39093b5bec207bf531680542dc300735e8e Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 8 Sep 2025 12:23:28 +0100 Subject: [PATCH 03/12] Refactor commands to use new prepareBackend method: group 2, where config parsing needs to be explicitly added --- internal/command/output.go | 9 +++++++-- internal/command/providers_schema.go | 9 ++++++++- internal/command/show.go | 7 ++++++- internal/command/state_identities.go | 13 +++++++++---- internal/command/state_mv.go | 9 ++++++++- internal/command/state_pull.go | 11 +++++++++-- internal/command/state_push.go | 13 +++++++++---- internal/command/state_replace_provider.go | 9 ++++++++- internal/command/state_rm.go | 9 ++++++++- internal/command/state_show.go | 11 +++++++++-- internal/command/taint.go | 8 +++++++- internal/command/untaint.go | 8 +++++++- 12 files changed, 95 insertions(+), 21 deletions(-) diff --git a/internal/command/output.go b/internal/command/output.go index 10828deea36d..cf2c3c1cfba8 100644 --- a/internal/command/output.go +++ b/internal/command/output.go @@ -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 } diff --git a/internal/command/providers_schema.go b/internal/command/providers_schema.go index 919e1a57078c..0917cb789139 100644 --- a/internal/command/providers_schema.go +++ b/internal/command/providers_schema.go @@ -56,7 +56,14 @@ func (c *ProvidersSchemaCommand) Run(args []string) int { var diags tfdiags.Diagnostics // Load the backend - b, backendDiags := c.Backend(nil) + mod, diags := c.Meta.loadSingleModule(".") + if diags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + + // Load the backend + b, backendDiags := c.Meta.prepareBackend(mod) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { c.showDiagnostics(diags) diff --git a/internal/command/show.go b/internal/command/show.go index afe1fe567e20..95c164967565 100644 --- a/internal/command/show.go +++ b/internal/command/show.go @@ -149,8 +149,13 @@ func (c *ShowCommand) show(path string) (*plans.Plan, *cloudplan.RemotePlanJSON, func (c *ShowCommand) showFromLatestStateSnapshot() (*statefile.File, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics + 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 backendDiags.HasErrors() { return nil, diags diff --git a/internal/command/state_identities.go b/internal/command/state_identities.go index 99ef4e09e7b0..df6d905735fc 100644 --- a/internal/command/state_identities.go +++ b/internal/command/state_identities.go @@ -11,7 +11,6 @@ import ( "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/states" - "github.com/hashicorp/terraform/internal/tfdiags" ) // StateIdentitiesCommand is a Command implementation that lists the resource identities @@ -46,10 +45,17 @@ func (c *StateIdentitiesCommand) Run(args []string) int { c.Meta.statePath = statePath } + 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(backendDiags) + c.showDiagnostics(diags) return 1 } @@ -79,7 +85,6 @@ func (c *StateIdentitiesCommand) Run(args []string) int { } var addrs []addrs.AbsResourceInstance - var diags tfdiags.Diagnostics if len(args) == 0 { addrs, diags = c.lookupAllResourceInstanceAddrs(state) } else { diff --git a/internal/command/state_mv.go b/internal/command/state_mv.go index 7ce5d0bf027d..34725e431437 100644 --- a/internal/command/state_mv.go +++ b/internal/command/state_mv.go @@ -391,7 +391,14 @@ func (c *StateMvCommand) Run(args []string) int { return 0 // This is as far as we go in dry-run mode } - b, backendDiags := c.Backend(nil) + mod, diags := c.Meta.loadSingleModule(".") + if diags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + + // Load the backend + b, backendDiags := c.Meta.prepareBackend(mod) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { c.showDiagnostics(diags) diff --git a/internal/command/state_pull.go b/internal/command/state_pull.go index 45e9e6145941..74898b8cde6c 100644 --- a/internal/command/state_pull.go +++ b/internal/command/state_pull.go @@ -32,10 +32,17 @@ func (c *StatePullCommand) Run(args []string) int { return 1 } + 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(backendDiags) + c.showDiagnostics(diags) return 1 } diff --git a/internal/command/state_push.go b/internal/command/state_push.go index 0f013f4eb50d..9aea5952d501 100644 --- a/internal/command/state_push.go +++ b/internal/command/state_push.go @@ -16,7 +16,6 @@ import ( "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terraform" - "github.com/hashicorp/terraform/internal/tfdiags" ) // StatePushCommand is a Command implementation that allows @@ -76,10 +75,17 @@ func (c *StatePushCommand) Run(args []string) int { return 1 } + 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(backendDiags) + c.showDiagnostics(diags) return 1 } @@ -135,7 +141,6 @@ func (c *StatePushCommand) Run(args []string) int { // Get schemas, if possible, before writing state var schemas *terraform.Schemas - var diags tfdiags.Diagnostics if isCloudMode(b) { schemas, diags = c.MaybeGetSchemas(srcStateFile.State, nil) } diff --git a/internal/command/state_replace_provider.go b/internal/command/state_replace_provider.go index 53796adedc9e..4771b971fc91 100644 --- a/internal/command/state_replace_provider.go +++ b/internal/command/state_replace_provider.go @@ -164,7 +164,14 @@ func (c *StateReplaceProviderCommand) Run(args []string) int { resource.ProviderConfig.Provider = to } - b, backendDiags := c.Backend(nil) + mod, diags := c.Meta.loadSingleModule(".") + if diags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + + // Load the backend + b, backendDiags := c.Meta.prepareBackend(mod) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { c.showDiagnostics(diags) diff --git a/internal/command/state_rm.go b/internal/command/state_rm.go index 805b8e91505b..5da226049710 100644 --- a/internal/command/state_rm.go +++ b/internal/command/state_rm.go @@ -115,7 +115,14 @@ func (c *StateRmCommand) Run(args []string) int { return 0 // This is as far as we go in dry-run mode } - b, backendDiags := c.Backend(nil) + mod, diags := c.Meta.loadSingleModule(".") + if diags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + + // Load the backend + b, backendDiags := c.Meta.prepareBackend(mod) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { c.showDiagnostics(diags) diff --git a/internal/command/state_show.go b/internal/command/state_show.go index b76d398ccd45..1a5477dd4ff3 100644 --- a/internal/command/state_show.go +++ b/internal/command/state_show.go @@ -47,10 +47,17 @@ func (c *StateShowCommand) Run(args []string) int { return 1 } + 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(backendDiags) + c.showDiagnostics(diags) return 1 } diff --git a/internal/command/taint.go b/internal/command/taint.go index cd9eeb6f01fe..ce2846a55029 100644 --- a/internal/command/taint.go +++ b/internal/command/taint.go @@ -65,8 +65,14 @@ func (c *TaintCommand) Run(args []string) int { return 1 } + 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) diff --git a/internal/command/untaint.go b/internal/command/untaint.go index ed82ed97299d..082af550dfce 100644 --- a/internal/command/untaint.go +++ b/internal/command/untaint.go @@ -55,8 +55,14 @@ func (c *UntaintCommand) Run(args []string) int { return 1 } + 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) From 44b9e4ab688ad0c72e9e7320217c82ce5a5c9c19 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 8 Sep 2025 12:27:10 +0100 Subject: [PATCH 04/12] Refactor commands to use new prepareBackend method: group 3, where we can use already parsed config --- internal/command/import.go | 4 +--- internal/command/providers.go | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/internal/command/import.go b/internal/command/import.go index b5b06c733487..5a870618369b 100644 --- a/internal/command/import.go +++ b/internal/command/import.go @@ -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) diff --git a/internal/command/providers.go b/internal/command/providers.go index 62f8fc3302df..ebbfcc04615c 100644 --- a/internal/command/providers.go +++ b/internal/command/providers.go @@ -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) From 5bacdcef4fffa0180d0033a8fc00c8257d8cd592 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 8 Sep 2025 12:41:44 +0100 Subject: [PATCH 05/12] Additional, more nested, places where logic for accessing backends needs to be refactored --- internal/command/show.go | 11 ++++++++--- internal/command/state_meta.go | 10 ++++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/internal/command/show.go b/internal/command/show.go index 95c164967565..8685018477a0 100644 --- a/internal/command/show.go +++ b/internal/command/show.go @@ -282,10 +282,15 @@ func (c *ShowCommand) getPlanFromPath(path string) (*plans.Plan, *cloudplan.Remo } func (c *ShowCommand) getDataFromCloudPlan(plan *cloudplan.SavedPlanBookmark, redacted bool) (*cloudplan.RemotePlanJSON, error) { + mod, diags := c.Meta.loadSingleModule(".") + if diags.HasErrors() { + return nil, diags.Err() + } + // Set up the backend - b, backendDiags := c.Backend(nil) - if backendDiags.HasErrors() { - return nil, errUnusable(backendDiags.Err(), "cloud plan") + b, diags := c.Meta.prepareBackend(mod) + if diags.HasErrors() { + return nil, errUnusable(diags.Err(), "cloud plan") } // Cloud plans only work if we're cloud. cl, ok := b.(*cloud.Cloud) diff --git a/internal/command/state_meta.go b/internal/command/state_meta.go index 75c935d4ba91..754c44b6a2fd 100644 --- a/internal/command/state_meta.go +++ b/internal/command/state_meta.go @@ -34,10 +34,16 @@ func (c *StateMeta) State() (statemgr.Full, error) { if c.statePath != "" { realState = statemgr.NewFilesystem(c.statePath) } else { + mod, diags := c.Meta.loadSingleModule(".") + if diags.HasErrors() { + return nil, diags.Err() + } + // Load the backend - b, backendDiags := c.Backend(nil) + b, backendDiags := c.Meta.prepareBackend(mod) + diags = diags.Append(backendDiags) if backendDiags.HasErrors() { - return nil, backendDiags.Err() + return nil, diags.Err() } workspace, err := c.Workspace() From 7d5f88d58879af6c989e856bd4359c52a66f97ca Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 8 Sep 2025 12:27:59 +0100 Subject: [PATCH 06/12] Remove duplicated comment --- internal/command/providers_schema.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/command/providers_schema.go b/internal/command/providers_schema.go index 0917cb789139..b9e982a75f5b 100644 --- a/internal/command/providers_schema.go +++ b/internal/command/providers_schema.go @@ -55,7 +55,6 @@ func (c *ProvidersSchemaCommand) Run(args []string) int { var diags tfdiags.Diagnostics - // Load the backend mod, diags := c.Meta.loadSingleModule(".") if diags.HasErrors() { c.showDiagnostics(diags) From 323b179f293265484708bbbf0d9d64f31883096b Mon Sep 17 00:00:00 2001 From: Sarah French Date: Tue, 9 Sep 2025 14:00:03 +0100 Subject: [PATCH 07/12] Add test coverage of `(m *Meta) prepareBackend()` --- internal/command/meta_backend.go | 9 +- internal/command/meta_backend_test.go | 102 ++++++++++++++++++ .../testdata/state-store-unchanged/main.tf | 3 +- 3 files changed, 108 insertions(+), 6 deletions(-) diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index e809905aac20..874bb5e9e3ac 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -1584,7 +1584,7 @@ func (m *Meta) updateSavedBackendHash(cHash int, sMgr *clistate.LocalState) tfdi // 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) { +func (m *Meta) prepareBackend(root *configs.Module, locks *depsfile.Locks) (backendrun.OperationsBackend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics var opts *BackendOpts @@ -1601,13 +1601,13 @@ func (m *Meta) prepareBackend(root *configs.Module) (backendrun.OperationsBacken case root.StateStore != nil: // In addition to config, use of a state_store requires // provider factory and provider locks data - factory, fDiags := m.getStateStoreProviderFactory(root.StateStore) + factory, fDiags := m.GetStateStoreProviderFactory(root.StateStore, locks) diags = diags.Append(fDiags) if fDiags.HasErrors() { return nil, diags } - // TODO - Use locks from here in opts below + // TODO(SarahFrench/radeksimko): Use locks from here in opts below _, lDiags := m.lockedDependencies() diags = diags.Append(lDiags) if lDiags.HasErrors() { @@ -1617,8 +1617,7 @@ func (m *Meta) prepareBackend(root *configs.Module) (backendrun.OperationsBacken opts = &BackendOpts{ StateStoreConfig: root.StateStore, ProviderFactory: factory, - // TODO - update once other work is merged into main - // Locks: locks, + Locks: locks, } default: // there is no config; defaults to local state storage diff --git a/internal/command/meta_backend_test.go b/internal/command/meta_backend_test.go index 116940796a72..0afaaaceb6c5 100644 --- a/internal/command/meta_backend_test.go +++ b/internal/command/meta_backend_test.go @@ -2962,6 +2962,108 @@ func Test_getStateStorageProviderVersion(t *testing.T) { }) } +func TestMetaBackend_prepareBackend(t *testing.T) { + locks := depsfile.NewLocks() + + 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, locks) + 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, locks) + 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") + } + }) + + 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, locks) + 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") + } + }) + + 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 := testStateStoreMock(t) + 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()) + } + + _, bDiags := m.prepareBackend(mod, locks) + if !bDiags.HasErrors() { + // TODO(SarahFrench/radeksimko): In future, we don't expect an error here! + t.Fatal("expected errors while state_store, due to incomplete implementation") + } + if !strings.Contains(bDiags.Err().Error(), "Changing a state store configuration is not implemented yet") { + t.Fatal("unexpected error: ", bDiags.Err()) + } + + // TODO(SarahFrench/radeksimko): Make assertion about returned operations backend. + }) +} + func testMetaBackend(t *testing.T, args []string) *Meta { var m Meta m.Ui = new(cli.MockUi) diff --git a/internal/command/testdata/state-store-unchanged/main.tf b/internal/command/testdata/state-store-unchanged/main.tf index d32e0d51615a..df1eaa76b650 100644 --- a/internal/command/testdata/state-store-unchanged/main.tf +++ b/internal/command/testdata/state-store-unchanged/main.tf @@ -1,7 +1,8 @@ terraform { required_providers { test = { - source = "hashicorp/test" + source = "hashicorp/test" + version = "1.2.3" } } state_store "test_store" { From 1afde447773bd0dfd55cd49090fe01e9c95fd790 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 20 Oct 2025 11:29:41 +0100 Subject: [PATCH 08/12] Add TODO related to using plans for backend/state_store config in apply commands --- internal/command/apply.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/command/apply.go b/internal/command/apply.go index 23722c1baf87..a8059eb7991e 100644 --- a/internal/command/apply.go +++ b/internal/command/apply.go @@ -219,6 +219,7 @@ 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) } else { // Both new plans and saved cloud plans load their backend from config. From 5ba4a6f20eac3ad9c36a1968c7b0d281030b7494 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 20 Oct 2025 11:58:22 +0100 Subject: [PATCH 09/12] Add `testStateStoreMockWithChunkNegotiation` test helper --- internal/command/meta_backend_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/internal/command/meta_backend_test.go b/internal/command/meta_backend_test.go index 0afaaaceb6c5..f2743538577a 100644 --- a/internal/command/meta_backend_test.go +++ b/internal/command/meta_backend_test.go @@ -3113,6 +3113,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}) From ae6db5c97fa5662ebcdf4ed7d73972fff43ccd90 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 20 Oct 2025 11:59:54 +0100 Subject: [PATCH 10/12] Add assertions to tests about the backend (remote-state, local, etc) in use within operations backend --- internal/command/meta_backend_test.go | 58 +++++++++++++++++++++------ 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/internal/command/meta_backend_test.go b/internal/command/meta_backend_test.go index f2743538577a..6ac498903e69 100644 --- a/internal/command/meta_backend_test.go +++ b/internal/command/meta_backend_test.go @@ -2963,7 +2963,6 @@ func Test_getStateStorageProviderVersion(t *testing.T) { } func TestMetaBackend_prepareBackend(t *testing.T) { - locks := depsfile.NewLocks() t.Run("it returns a cloud backend from cloud backend config", func(t *testing.T) { // Create a temporary working directory with cloud configuration in @@ -2981,7 +2980,7 @@ func TestMetaBackend_prepareBackend(t *testing.T) { // We cannot initialize a cloud backend so we instead check // the init error is referencing HCP Terraform - _, bDiags := m.prepareBackend(mod, locks) + _, bDiags := m.prepareBackend(mod) if !bDiags.HasErrors() { t.Fatal("expected error but got none") } @@ -2992,6 +2991,7 @@ func TestMetaBackend_prepareBackend(t *testing.T) { 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() @@ -3006,7 +3006,7 @@ func TestMetaBackend_prepareBackend(t *testing.T) { t.Fatalf("unexpected error when loading test config: %s", loadDiags.Err()) } - b, bDiags := m.prepareBackend(mod, locks) + b, bDiags := m.prepareBackend(mod) if bDiags.HasErrors() { t.Fatal("unexpected error: ", bDiags.Err()) } @@ -3014,13 +3014,21 @@ func TestMetaBackend_prepareBackend(t *testing.T) { 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, locks) + b, bDiags := m.prepareBackend(emptyConfig.Module) if bDiags.HasErrors() { t.Fatal("unexpected error: ", bDiags.Err()) } @@ -3028,6 +3036,14 @@ func TestMetaBackend_prepareBackend(t *testing.T) { 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) { @@ -3038,7 +3054,7 @@ func TestMetaBackend_prepareBackend(t *testing.T) { m := testMetaBackend(t, nil) m.AllowExperimentalFeatures = true - mock := testStateStoreMock(t) + 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), @@ -3051,16 +3067,34 @@ func TestMetaBackend_prepareBackend(t *testing.T) { t.Fatalf("unexpected error when loading test config: %s", loadDiags.Err()) } - _, bDiags := m.prepareBackend(mod, locks) - if !bDiags.HasErrors() { - // TODO(SarahFrench/radeksimko): In future, we don't expect an error here! - t.Fatal("expected errors while state_store, due to incomplete implementation") + // 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) } - if !strings.Contains(bDiags.Err().Error(), "Changing a state store configuration is not implemented yet") { - t.Fatal("unexpected error: ", bDiags.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()) } - // TODO(SarahFrench/radeksimko): Make assertion about returned operations backend. + 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) + } }) } From be43ec66c515e15f6772b929bf12d6204b516dce Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 20 Oct 2025 12:05:09 +0100 Subject: [PATCH 11/12] Stop prepareBackend taking locks as argument --- internal/command/meta_backend.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index 874bb5e9e3ac..b1368e879e61 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -1584,7 +1584,7 @@ func (m *Meta) updateSavedBackendHash(cHash int, sMgr *clistate.LocalState) tfdi // 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, locks *depsfile.Locks) (backendrun.OperationsBackend, tfdiags.Diagnostics) { +func (m *Meta) prepareBackend(root *configs.Module) (backendrun.OperationsBackend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics var opts *BackendOpts @@ -1601,16 +1601,15 @@ func (m *Meta) prepareBackend(root *configs.Module, locks *depsfile.Locks) (back case root.StateStore != nil: // In addition to config, use of a state_store requires // provider factory and provider locks data - factory, fDiags := m.GetStateStoreProviderFactory(root.StateStore, locks) - diags = diags.Append(fDiags) - if fDiags.HasErrors() { + locks, lDiags := m.lockedDependencies() + diags = diags.Append(lDiags) + if lDiags.HasErrors() { return nil, diags } - // TODO(SarahFrench/radeksimko): Use locks from here in opts below - _, lDiags := m.lockedDependencies() - diags = diags.Append(lDiags) - if lDiags.HasErrors() { + factory, fDiags := m.GetStateStoreProviderFactory(root.StateStore, locks) + diags = diags.Append(fDiags) + if fDiags.HasErrors() { return nil, diags } From 823c5b483b19b06af183c5f755a3fc292ad30aa8 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 20 Oct 2025 13:19:54 +0100 Subject: [PATCH 12/12] Code comment in prepareBackend --- internal/command/meta_backend.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index b1368e879e61..e034d4f6e4f3 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -1622,7 +1622,10 @@ func (m *Meta) prepareBackend(root *configs.Module) (backendrun.OperationsBacken // there is no config; defaults to local state storage opts = &BackendOpts{} } - opts.Init = false // To be explicit- this method should not be used in init commands! + + // 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)