Skip to content

Commit f494ff5

Browse files
authored
Pull init command's Run method logic into separate method in new file, enable accessing experimental version of init logic via experiments and flags or ENVs (#37327)
* Pull `init` `Run` method into new method in a separate file, in preparation for adding experimental fork. * Add license header * Allow init args to be accessed before calling separate init run logic * Add -enable-pss flag to the init command, to be used for accessing experimental init logic * Fix- put call to `run` in else block! * Make flag name more explicit * Add an environment variable alternative to the CLI flag * Make the panic text more user-friendly * go fmt
1 parent dec0edf commit f494ff5

File tree

3 files changed

+324
-278
lines changed

3 files changed

+324
-278
lines changed

internal/command/arguments/init.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ type Init struct {
7373
PluginPath FlagStringSlice
7474

7575
Args []string
76+
77+
// The -enable-pluggable-state-storage-experiment flag is used in control flow logic in the init command.
78+
// TODO(SarahFrench/radeksimko): Remove this once the feature is no longer
79+
// experimental
80+
EnablePssExperiment bool
7681
}
7782

7883
// ParseInit processes CLI arguments, returning an Init value and errors.
@@ -107,6 +112,9 @@ func ParseInit(args []string) (*Init, tfdiags.Diagnostics) {
107112
cmdFlags.Var(&init.BackendConfig, "backend-config", "")
108113
cmdFlags.Var(&init.PluginPath, "plugin-dir", "plugin directory")
109114

115+
// Used for enabling experimental code that's invoked before configuration is parsed.
116+
cmdFlags.BoolVar(&init.EnablePssExperiment, "enable-pluggable-state-storage-experiment", false, "Enable the pluggable state storage experiment")
117+
110118
if err := cmdFlags.Parse(args); err != nil {
111119
diags = diags.Append(tfdiags.Sourceless(
112120
tfdiags.Error,

internal/command/init.go

Lines changed: 13 additions & 278 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ package command
55

66
import (
77
"context"
8-
"errors"
98
"fmt"
109
"log"
1110
"maps"
11+
"os"
1212
"reflect"
1313
"slices"
1414
"sort"
@@ -19,13 +19,11 @@ import (
1919
"github.com/posener/complete"
2020
"github.com/zclconf/go-cty/cty"
2121
"go.opentelemetry.io/otel/attribute"
22-
"go.opentelemetry.io/otel/codes"
2322
"go.opentelemetry.io/otel/trace"
2423

2524
"github.com/hashicorp/terraform/internal/addrs"
2625
"github.com/hashicorp/terraform/internal/backend"
2726
backendInit "github.com/hashicorp/terraform/internal/backend/init"
28-
"github.com/hashicorp/terraform/internal/cloud"
2927
"github.com/hashicorp/terraform/internal/command/arguments"
3028
"github.com/hashicorp/terraform/internal/command/views"
3129
"github.com/hashicorp/terraform/internal/configs"
@@ -34,7 +32,6 @@ import (
3432
"github.com/hashicorp/terraform/internal/getproviders"
3533
"github.com/hashicorp/terraform/internal/providercache"
3634
"github.com/hashicorp/terraform/internal/states"
37-
"github.com/hashicorp/terraform/internal/terraform"
3835
"github.com/hashicorp/terraform/internal/tfdiags"
3936
tfversion "github.com/hashicorp/terraform/version"
4037
)
@@ -58,282 +55,20 @@ func (c *InitCommand) Run(args []string) int {
5855
return 1
5956
}
6057

61-
c.forceInitCopy = initArgs.ForceInitCopy
62-
c.Meta.stateLock = initArgs.StateLock
63-
c.Meta.stateLockTimeout = initArgs.StateLockTimeout
64-
c.reconfigure = initArgs.Reconfigure
65-
c.migrateState = initArgs.MigrateState
66-
c.Meta.ignoreRemoteVersion = initArgs.IgnoreRemoteVersion
67-
c.Meta.input = initArgs.InputEnabled
68-
c.Meta.targetFlags = initArgs.TargetFlags
69-
c.Meta.compactWarnings = initArgs.CompactWarnings
70-
71-
varArgs := initArgs.Vars.All()
72-
items := make([]arguments.FlagNameValue, len(varArgs))
73-
for i := range varArgs {
74-
items[i].Name = varArgs[i].Name
75-
items[i].Value = varArgs[i].Value
58+
// The else condition below invokes the original logic of the init command.
59+
// An experimental version of the init code will be used if:
60+
// > The user uses an experimental version of TF (alpha or built from source)
61+
// > Either the flag -enable-pluggable-state-storage-experiment is passed to the init command.
62+
// > Or, the environment variable TF_ENABLE_PLUGGABLE_STATE_STORAGE is set to any value.
63+
if v := os.Getenv("TF_ENABLE_PLUGGABLE_STATE_STORAGE"); v != "" {
64+
initArgs.EnablePssExperiment = true
7665
}
77-
c.Meta.variableArgs = arguments.FlagNameValueSlice{Items: &items}
78-
79-
// Copying the state only happens during backend migration, so setting
80-
// -force-copy implies -migrate-state
81-
if c.forceInitCopy {
82-
c.migrateState = true
83-
}
84-
85-
if len(initArgs.PluginPath) > 0 {
86-
c.pluginPath = initArgs.PluginPath
87-
}
88-
89-
// Validate the arg count and get the working directory
90-
path, err := ModulePath(initArgs.Args)
91-
if err != nil {
92-
diags = diags.Append(err)
93-
view.Diagnostics(diags)
94-
return 1
95-
}
96-
97-
if err := c.storePluginPath(c.pluginPath); err != nil {
98-
diags = diags.Append(fmt.Errorf("Error saving -plugin-dir to workspace directory: %s", err))
99-
view.Diagnostics(diags)
100-
return 1
101-
}
102-
103-
// Initialization can be aborted by interruption signals
104-
ctx, done := c.InterruptibleContext(c.CommandContext())
105-
defer done()
106-
107-
// This will track whether we outputted anything so that we know whether
108-
// to output a newline before the success message
109-
var header bool
110-
111-
if initArgs.FromModule != "" {
112-
src := initArgs.FromModule
113-
114-
empty, err := configs.IsEmptyDir(path, initArgs.TestsDirectory)
115-
if err != nil {
116-
diags = diags.Append(fmt.Errorf("Error validating destination directory: %s", err))
117-
view.Diagnostics(diags)
118-
return 1
119-
}
120-
if !empty {
121-
diags = diags.Append(errors.New(strings.TrimSpace(errInitCopyNotEmpty)))
122-
view.Diagnostics(diags)
123-
return 1
124-
}
125-
126-
view.Output(views.CopyingConfigurationMessage, src)
127-
header = true
128-
129-
hooks := uiModuleInstallHooks{
130-
Ui: c.Ui,
131-
ShowLocalPaths: false, // since they are in a weird location for init
132-
View: view,
133-
}
134-
135-
ctx, span := tracer.Start(ctx, "-from-module=...", trace.WithAttributes(
136-
attribute.String("module_source", src),
137-
))
138-
139-
initDirFromModuleAbort, initDirFromModuleDiags := c.initDirFromModule(ctx, path, src, hooks)
140-
diags = diags.Append(initDirFromModuleDiags)
141-
if initDirFromModuleAbort || initDirFromModuleDiags.HasErrors() {
142-
view.Diagnostics(diags)
143-
span.SetStatus(codes.Error, "module installation failed")
144-
span.End()
145-
return 1
146-
}
147-
span.End()
148-
149-
view.Output(views.EmptyMessage)
150-
}
151-
152-
// If our directory is empty, then we're done. We can't get or set up
153-
// the backend with an empty directory.
154-
empty, err := configs.IsEmptyDir(path, initArgs.TestsDirectory)
155-
if err != nil {
156-
diags = diags.Append(fmt.Errorf("Error checking configuration: %s", err))
157-
view.Diagnostics(diags)
158-
return 1
159-
}
160-
if empty {
161-
view.Output(views.OutputInitEmptyMessage)
162-
return 0
163-
}
164-
165-
// Load just the root module to begin backend and module initialization
166-
rootModEarly, earlyConfDiags := c.loadSingleModuleWithTests(path, initArgs.TestsDirectory)
167-
168-
// There may be parsing errors in config loading but these will be shown later _after_
169-
// checking for core version requirement errors. Not meeting the version requirement should
170-
// be the first error displayed if that is an issue, but other operations are required
171-
// before being able to check core version requirements.
172-
if rootModEarly == nil {
173-
diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError)), earlyConfDiags)
174-
view.Diagnostics(diags)
175-
176-
return 1
177-
}
178-
179-
var back backend.Backend
180-
181-
// There may be config errors or backend init errors but these will be shown later _after_
182-
// checking for core version requirement errors.
183-
var backDiags tfdiags.Diagnostics
184-
var backendOutput bool
185-
186-
switch {
187-
case initArgs.Cloud && rootModEarly.CloudConfig != nil:
188-
back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view)
189-
case initArgs.Backend:
190-
back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view)
191-
default:
192-
// load the previously-stored backend config
193-
back, backDiags = c.Meta.backendFromState(ctx)
194-
}
195-
if backendOutput {
196-
header = true
197-
}
198-
199-
var state *states.State
200-
201-
// If we have a functional backend (either just initialized or initialized
202-
// on a previous run) we'll use the current state as a potential source
203-
// of provider dependencies.
204-
if back != nil {
205-
c.ignoreRemoteVersionConflict(back)
206-
workspace, err := c.Workspace()
207-
if err != nil {
208-
diags = diags.Append(fmt.Errorf("Error selecting workspace: %s", err))
209-
view.Diagnostics(diags)
210-
return 1
211-
}
212-
sMgr, err := back.StateMgr(workspace)
213-
if err != nil {
214-
diags = diags.Append(fmt.Errorf("Error loading state: %s", err))
215-
view.Diagnostics(diags)
216-
return 1
217-
}
218-
219-
if err := sMgr.RefreshState(); err != nil {
220-
diags = diags.Append(fmt.Errorf("Error refreshing state: %s", err))
221-
view.Diagnostics(diags)
222-
return 1
223-
}
224-
225-
state = sMgr.State()
226-
}
227-
228-
if initArgs.Get {
229-
modsOutput, modsAbort, modsDiags := c.getModules(ctx, path, initArgs.TestsDirectory, rootModEarly, initArgs.Upgrade, view)
230-
diags = diags.Append(modsDiags)
231-
if modsAbort || modsDiags.HasErrors() {
232-
view.Diagnostics(diags)
233-
return 1
234-
}
235-
if modsOutput {
236-
header = true
237-
}
238-
}
239-
240-
// With all of the modules (hopefully) installed, we can now try to load the
241-
// whole configuration tree.
242-
config, confDiags := c.loadConfigWithTests(path, initArgs.TestsDirectory)
243-
// configDiags will be handled after the version constraint check, since an
244-
// incorrect version of terraform may be producing errors for configuration
245-
// constructs added in later versions.
246-
247-
// Before we go further, we'll check to make sure none of the modules in
248-
// the configuration declare that they don't support this Terraform
249-
// version, so we can produce a version-related error message rather than
250-
// potentially-confusing downstream errors.
251-
versionDiags := terraform.CheckCoreVersionRequirements(config)
252-
if versionDiags.HasErrors() {
253-
view.Diagnostics(versionDiags)
254-
return 1
255-
}
256-
257-
// We've passed the core version check, now we can show errors from the
258-
// configuration and backend initialisation.
259-
260-
// Now, we can check the diagnostics from the early configuration and the
261-
// backend.
262-
diags = diags.Append(earlyConfDiags)
263-
diags = diags.Append(backDiags)
264-
if earlyConfDiags.HasErrors() {
265-
diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError)))
266-
view.Diagnostics(diags)
267-
return 1
268-
}
269-
270-
// Now, we can show any errors from initializing the backend, but we won't
271-
// show the InitConfigError preamble as we didn't detect problems with
272-
// the early configuration.
273-
if backDiags.HasErrors() {
274-
view.Diagnostics(diags)
275-
return 1
276-
}
277-
278-
// If everything is ok with the core version check and backend initialization,
279-
// show other errors from loading the full configuration tree.
280-
diags = diags.Append(confDiags)
281-
if confDiags.HasErrors() {
282-
diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError)))
283-
view.Diagnostics(diags)
284-
return 1
285-
}
286-
287-
if cb, ok := back.(*cloud.Cloud); ok {
288-
if c.RunningInAutomation {
289-
if err := cb.AssertImportCompatible(config); err != nil {
290-
diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Compatibility error", err.Error()))
291-
view.Diagnostics(diags)
292-
return 1
293-
}
294-
}
295-
}
296-
297-
// Now that we have loaded all modules, check the module tree for missing providers.
298-
providersOutput, providersAbort, providerDiags := c.getProviders(ctx, config, state, initArgs.Upgrade, initArgs.PluginPath, initArgs.Lockfile, view)
299-
diags = diags.Append(providerDiags)
300-
if providersAbort || providerDiags.HasErrors() {
301-
view.Diagnostics(diags)
302-
return 1
303-
}
304-
if providersOutput {
305-
header = true
306-
}
307-
308-
// If we outputted information, then we need to output a newline
309-
// so that our success message is nicely spaced out from prior text.
310-
if header {
311-
view.Output(views.EmptyMessage)
312-
}
313-
314-
// If we accumulated any warnings along the way that weren't accompanied
315-
// by errors then we'll output them here so that the success message is
316-
// still the final thing shown.
317-
view.Diagnostics(diags)
318-
_, cloud := back.(*cloud.Cloud)
319-
output := views.OutputInitSuccessMessage
320-
if cloud {
321-
output = views.OutputInitSuccessCloudMessage
322-
}
323-
324-
view.Output(output)
325-
326-
if !c.RunningInAutomation {
327-
// If we're not running in an automation wrapper, give the user
328-
// some more detailed next steps that are appropriate for interactive
329-
// shell usage.
330-
output = views.OutputInitSuccessCLIMessage
331-
if cloud {
332-
output = views.OutputInitSuccessCLICloudMessage
333-
}
334-
view.Output(output)
66+
if c.Meta.AllowExperimentalFeatures && initArgs.EnablePssExperiment {
67+
// TODO(SarahFrench/radeksimko): Remove forked init logic once feature is no longer experimental
68+
panic("This experiment is not available yet")
69+
} else {
70+
return c.run(initArgs, view)
33571
}
336-
return 0
33772
}
33873

33974
func (c *InitCommand) getModules(ctx context.Context, path, testsDir string, earlyRoot *configs.Module, upgrade bool, view views.Init) (output bool, abort bool, diags tfdiags.Diagnostics) {

0 commit comments

Comments
 (0)