Skip to content

Commit f5a28cf

Browse files
authored
PSS: Update how commands access backends, so both backend and state_store configuration can be used (#37569)
* Add a generic method for loading an operations backend in non-init commands * Refactor commands to use new prepareBackend method: group 1 * Refactor commands to use new prepareBackend method: group 2, where config parsing needs to be explicitly added * Refactor commands to use new prepareBackend method: group 3, where we can use already parsed config * Additional, more nested, places where logic for accessing backends needs to be refactored * Remove duplicated comment * Add test coverage of `(m *Meta) prepareBackend()` * Add TODO related to using plans for backend/state_store config in apply commands * Add `testStateStoreMockWithChunkNegotiation` test helper * Add assertions to tests about the backend (remote-state, local, etc) in use within operations backend * Stop prepareBackend taking locks as argument * Code comment in prepareBackend * Replace c.Meta.prepareBackend with c.prepareBackend * Change `c.Meta.loadSingleModule` to `c.loadSingleModule` * Rename (Meta).prepareBackend to (Meta).backend, update godoc comment to make relationship to (Meta).Backend more obvious. * Revert change from config.Module to config.Root.Module * Update `(m *Meta) backend` method to parse config itself, and also to adhere to calling code's viewtype instructions * Update all tests and calling code following previous commit * Change how an operations backend is obtained by autocomplete code * Update autocomplete to return nil if no workspace names are returned from the backend * Add test coverage for autocompleting workspace names when using a pluggable state store * Fix output command: pass view type data to new `backend` method * Fix in plan command: pass correct view type to `backend` method * Fix `providers schema` command to use correct viewtype when preparing a backend
1 parent f0a2953 commit f5a28cf

33 files changed

+419
-206
lines changed

internal/command/apply.go

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -219,19 +219,15 @@ func (c *ApplyCommand) PrepareBackend(planFile *planfile.WrappedPlanFile, args *
219219
))
220220
return nil, diags
221221
}
222+
// TODO: Update BackendForLocalPlan to use state storage, and plan to be able to contain State Store config details
222223
be, beDiags = c.BackendForLocalPlan(plan.Backend)
223224
} else {
224-
// Both new plans and saved cloud plans load their backend from config.
225-
backendConfig, configDiags := c.loadBackendConfig(".")
226-
diags = diags.Append(configDiags)
227-
if configDiags.HasErrors() {
228-
return nil, diags
229-
}
230225

231-
be, beDiags = c.Backend(&BackendOpts{
232-
BackendConfig: backendConfig,
233-
ViewType: viewType,
234-
})
226+
// Load the backend
227+
//
228+
// Note: Both new plans and saved cloud plans load their backend from config,
229+
// hence the config parsing in the method below.
230+
be, beDiags = c.backend(".", viewType)
235231
}
236232

237233
diags = diags.Append(beDiags)

internal/command/autocomplete.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package command
55

66
import (
7+
"github.com/hashicorp/terraform/internal/command/arguments"
78
"github.com/posener/complete"
89
)
910

@@ -48,19 +49,18 @@ func (m *Meta) completePredictWorkspaceName() complete.Predictor {
4849
return nil
4950
}
5051

51-
backendConfig, diags := m.loadBackendConfig(configPath)
52+
b, diags := m.backend(configPath, arguments.ViewHuman)
5253
if diags.HasErrors() {
5354
return nil
5455
}
5556

56-
b, diags := m.Backend(&BackendOpts{
57-
BackendConfig: backendConfig,
58-
})
59-
if diags.HasErrors() {
57+
names, _ := b.Workspaces()
58+
if len(names) == 0 {
59+
// Presence of the "default" isn't always guaranteed
60+
// Backends will report it as always existing, pluggable
61+
// state stores will only do so if it _actually_ exists.
6062
return nil
6163
}
62-
63-
names, _ := b.Workspaces()
6464
return names
6565
})
6666
}

internal/command/autocomplete_test.go

Lines changed: 107 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,124 @@
44
package command
55

66
import (
7-
"io/ioutil"
8-
"os"
97
"reflect"
108
"testing"
119

1210
"github.com/hashicorp/cli"
11+
"github.com/hashicorp/terraform/internal/addrs"
12+
"github.com/hashicorp/terraform/internal/command/workdir"
13+
"github.com/hashicorp/terraform/internal/providers"
1314
"github.com/posener/complete"
1415
)
1516

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

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

28-
ui := new(cli.MockUi)
29-
meta := &Meta{Ui: ui}
24+
ui := new(cli.MockUi)
25+
meta := &Meta{Ui: ui}
3026

31-
predictor := meta.completePredictWorkspaceName()
27+
predictor := meta.completePredictWorkspaceName()
3228

33-
got := predictor.Predict(complete.Args{
34-
Last: "",
29+
got := predictor.Predict(complete.Args{
30+
Last: "",
31+
})
32+
want := []string{"default"}
33+
if !reflect.DeepEqual(got, want) {
34+
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want)
35+
}
36+
})
37+
38+
t.Run("test autocompletion using a state store", func(t *testing.T) {
39+
// Create a temporary working directory with state_store config
40+
td := t.TempDir()
41+
testCopyDir(t, testFixturePath("state-store-unchanged"), td)
42+
t.Chdir(td)
43+
44+
// Set up pluggable state store provider mock
45+
mockProvider := mockPluggableStateStorageProvider()
46+
// Mock the existence of workspaces
47+
mockProvider.MockStates = map[string]interface{}{
48+
"default": true,
49+
"foobar": true,
50+
}
51+
mockProviderAddress := addrs.NewDefaultProvider("test")
52+
providerSource, close := newMockProviderSource(t, map[string][]string{
53+
"hashicorp/test": {"1.0.0"},
54+
})
55+
defer close()
56+
57+
ui := new(cli.MockUi)
58+
view, _ := testView(t)
59+
wd := workdir.NewDir(".")
60+
wd.OverrideOriginalWorkingDir(td)
61+
meta := Meta{
62+
WorkingDir: wd, // Use the test's temp dir
63+
Ui: ui,
64+
View: view,
65+
AllowExperimentalFeatures: true,
66+
testingOverrides: &testingOverrides{
67+
Providers: map[addrs.Provider]providers.Factory{
68+
mockProviderAddress: providers.FactoryFixed(mockProvider),
69+
},
70+
},
71+
ProviderSource: providerSource,
72+
}
73+
74+
predictor := meta.completePredictWorkspaceName()
75+
76+
got := predictor.Predict(complete.Args{
77+
Last: "",
78+
})
79+
want := []string{"default", "foobar"}
80+
if !reflect.DeepEqual(got, want) {
81+
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want)
82+
}
83+
})
84+
85+
t.Run("test autocompletion using a state store containing no workspaces", func(t *testing.T) {
86+
// Create a temporary working directory with state_store config
87+
td := t.TempDir()
88+
testCopyDir(t, testFixturePath("state-store-unchanged"), td)
89+
t.Chdir(td)
90+
91+
// Set up pluggable state store provider mock
92+
mockProvider := mockPluggableStateStorageProvider()
93+
// No workspaces exist in the mock
94+
mockProvider.MockStates = map[string]interface{}{}
95+
mockProviderAddress := addrs.NewDefaultProvider("test")
96+
providerSource, close := newMockProviderSource(t, map[string][]string{
97+
"hashicorp/test": {"1.0.0"},
98+
})
99+
defer close()
100+
101+
ui := new(cli.MockUi)
102+
view, _ := testView(t)
103+
wd := workdir.NewDir(".")
104+
wd.OverrideOriginalWorkingDir(td)
105+
meta := Meta{
106+
WorkingDir: wd, // Use the test's temp dir
107+
Ui: ui,
108+
View: view,
109+
AllowExperimentalFeatures: true,
110+
testingOverrides: &testingOverrides{
111+
Providers: map[addrs.Provider]providers.Factory{
112+
mockProviderAddress: providers.FactoryFixed(mockProvider),
113+
},
114+
},
115+
ProviderSource: providerSource,
116+
}
117+
118+
predictor := meta.completePredictWorkspaceName()
119+
120+
got := predictor.Predict(complete.Args{
121+
Last: "",
122+
})
123+
if got != nil {
124+
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, nil)
125+
}
35126
})
36-
want := []string{"default"}
37-
if !reflect.DeepEqual(got, want) {
38-
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want)
39-
}
40127
}

internal/command/console.go

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,8 @@ func (c *ConsoleCommand) Run(args []string) int {
5353

5454
var diags tfdiags.Diagnostics
5555

56-
backendConfig, backendDiags := c.loadBackendConfig(configPath)
57-
diags = diags.Append(backendDiags)
58-
if diags.HasErrors() {
59-
c.showDiagnostics(diags)
60-
return 1
61-
}
62-
6356
// Load the backend
64-
b, backendDiags := c.Backend(&BackendOpts{
65-
BackendConfig: backendConfig,
66-
})
57+
b, backendDiags := c.backend(configPath, arguments.ViewHuman)
6758
diags = diags.Append(backendDiags)
6859
if backendDiags.HasErrors() {
6960
c.showDiagnostics(diags)

internal/command/graph.go

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,17 +68,8 @@ func (c *GraphCommand) Run(args []string) int {
6868

6969
var diags tfdiags.Diagnostics
7070

71-
backendConfig, backendDiags := c.loadBackendConfig(configPath)
72-
diags = diags.Append(backendDiags)
73-
if diags.HasErrors() {
74-
c.showDiagnostics(diags)
75-
return 1
76-
}
77-
7871
// Load the backend
79-
b, backendDiags := c.Backend(&BackendOpts{
80-
BackendConfig: backendConfig,
81-
})
72+
b, backendDiags := c.backend(".", arguments.ViewHuman)
8273
diags = diags.Append(backendDiags)
8374
if backendDiags.HasErrors() {
8475
c.showDiagnostics(diags)

internal/command/import.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,7 @@ func (c *ImportCommand) Run(args []string) int {
164164
}
165165

166166
// Load the backend
167-
b, backendDiags := c.Backend(&BackendOpts{
168-
BackendConfig: config.Module.Backend,
169-
})
167+
b, backendDiags := c.backend(".", arguments.ViewHuman)
170168
diags = diags.Append(backendDiags)
171169
if backendDiags.HasErrors() {
172170
c.showDiagnostics(diags)

internal/command/meta_backend.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1580,6 +1580,82 @@ func (m *Meta) updateSavedBackendHash(cHash int, sMgr *clistate.LocalState) tfdi
15801580
return diags
15811581
}
15821582

1583+
// backend returns an operations backend that may use a backend, cloud, or state_store block for state storage.
1584+
// Based on the supplied config, it prepares arguments to pass into (Meta).Backend, which returns the operations backend.
1585+
//
1586+
// This method should be used in NON-init operations only; it's incapable of processing new init command CLI flags used
1587+
// for partial configuration, however it will use the backend state file to use partial configuration from a previous
1588+
// init command.
1589+
func (m *Meta) backend(configPath string, viewType arguments.ViewType) (backendrun.OperationsBackend, tfdiags.Diagnostics) {
1590+
var diags tfdiags.Diagnostics
1591+
1592+
if configPath == "" {
1593+
configPath = "."
1594+
}
1595+
1596+
// Only return error diagnostics at this point. Any warnings will be caught
1597+
// again later and duplicated in the output.
1598+
root, mDiags := m.loadSingleModule(configPath)
1599+
if mDiags.HasErrors() {
1600+
diags = diags.Append(mDiags)
1601+
return nil, diags
1602+
}
1603+
1604+
var opts *BackendOpts
1605+
switch {
1606+
case root.Backend != nil:
1607+
opts = &BackendOpts{
1608+
BackendConfig: root.Backend,
1609+
ViewType: viewType,
1610+
}
1611+
case root.CloudConfig != nil:
1612+
backendConfig := root.CloudConfig.ToBackendConfig()
1613+
opts = &BackendOpts{
1614+
BackendConfig: &backendConfig,
1615+
ViewType: viewType,
1616+
}
1617+
case root.StateStore != nil:
1618+
// In addition to config, use of a state_store requires
1619+
// provider factory and provider locks data
1620+
locks, lDiags := m.lockedDependencies()
1621+
diags = diags.Append(lDiags)
1622+
if lDiags.HasErrors() {
1623+
return nil, diags
1624+
}
1625+
1626+
factory, fDiags := m.GetStateStoreProviderFactory(root.StateStore, locks)
1627+
diags = diags.Append(fDiags)
1628+
if fDiags.HasErrors() {
1629+
return nil, diags
1630+
}
1631+
1632+
opts = &BackendOpts{
1633+
StateStoreConfig: root.StateStore,
1634+
ProviderFactory: factory,
1635+
Locks: locks,
1636+
ViewType: viewType,
1637+
}
1638+
default:
1639+
// there is no config; defaults to local state storage
1640+
opts = &BackendOpts{
1641+
ViewType: viewType,
1642+
}
1643+
}
1644+
1645+
// This method should not be used for init commands,
1646+
// so we always set this value as false.
1647+
opts.Init = false
1648+
1649+
// Load the backend
1650+
be, beDiags := m.Backend(opts)
1651+
diags = diags.Append(beDiags)
1652+
if beDiags.HasErrors() {
1653+
return nil, diags
1654+
}
1655+
1656+
return be, diags
1657+
}
1658+
15831659
//-------------------------------------------------------------------
15841660
// State Store Config Scenarios
15851661
// The functions below cover handling all the various scenarios that

0 commit comments

Comments
 (0)