-
Notifications
You must be signed in to change notification settings - Fork 9.9k
Pull init
command's Run method logic into separate method in new file, enable accessing experimental version of init logic via experiments and flags
#37327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
fdd193a
bf35c9f
8d37274
138bc44
062a7a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,6 @@ package command | |
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"log" | ||
"reflect" | ||
|
@@ -17,21 +16,18 @@ import ( | |
"github.com/posener/complete" | ||
"github.com/zclconf/go-cty/cty" | ||
"go.opentelemetry.io/otel/attribute" | ||
"go.opentelemetry.io/otel/codes" | ||
"go.opentelemetry.io/otel/trace" | ||
|
||
"github.com/hashicorp/terraform/internal/addrs" | ||
"github.com/hashicorp/terraform/internal/backend" | ||
backendInit "github.com/hashicorp/terraform/internal/backend/init" | ||
"github.com/hashicorp/terraform/internal/cloud" | ||
"github.com/hashicorp/terraform/internal/command/arguments" | ||
"github.com/hashicorp/terraform/internal/command/views" | ||
"github.com/hashicorp/terraform/internal/configs" | ||
"github.com/hashicorp/terraform/internal/configs/configschema" | ||
"github.com/hashicorp/terraform/internal/getproviders" | ||
"github.com/hashicorp/terraform/internal/providercache" | ||
"github.com/hashicorp/terraform/internal/states" | ||
"github.com/hashicorp/terraform/internal/terraform" | ||
"github.com/hashicorp/terraform/internal/tfdiags" | ||
tfversion "github.com/hashicorp/terraform/version" | ||
) | ||
|
@@ -55,282 +51,16 @@ func (c *InitCommand) Run(args []string) int { | |
return 1 | ||
} | ||
|
||
c.forceInitCopy = initArgs.ForceInitCopy | ||
c.Meta.stateLock = initArgs.StateLock | ||
c.Meta.stateLockTimeout = initArgs.StateLockTimeout | ||
c.reconfigure = initArgs.Reconfigure | ||
c.migrateState = initArgs.MigrateState | ||
c.Meta.ignoreRemoteVersion = initArgs.IgnoreRemoteVersion | ||
c.Meta.input = initArgs.InputEnabled | ||
c.Meta.targetFlags = initArgs.TargetFlags | ||
c.Meta.compactWarnings = initArgs.CompactWarnings | ||
|
||
varArgs := initArgs.Vars.All() | ||
items := make([]arguments.FlagNameValue, len(varArgs)) | ||
for i := range varArgs { | ||
items[i].Name = varArgs[i].Name | ||
items[i].Value = varArgs[i].Value | ||
} | ||
c.Meta.variableArgs = arguments.FlagNameValueSlice{Items: &items} | ||
|
||
// Copying the state only happens during backend migration, so setting | ||
// -force-copy implies -migrate-state | ||
if c.forceInitCopy { | ||
c.migrateState = true | ||
} | ||
|
||
if len(initArgs.PluginPath) > 0 { | ||
c.pluginPath = initArgs.PluginPath | ||
} | ||
|
||
// Validate the arg count and get the working directory | ||
path, err := ModulePath(initArgs.Args) | ||
if err != nil { | ||
diags = diags.Append(err) | ||
view.Diagnostics(diags) | ||
return 1 | ||
} | ||
|
||
if err := c.storePluginPath(c.pluginPath); err != nil { | ||
diags = diags.Append(fmt.Errorf("Error saving -plugin-dir to workspace directory: %s", err)) | ||
view.Diagnostics(diags) | ||
return 1 | ||
} | ||
|
||
// Initialization can be aborted by interruption signals | ||
ctx, done := c.InterruptibleContext(c.CommandContext()) | ||
defer done() | ||
|
||
// This will track whether we outputted anything so that we know whether | ||
// to output a newline before the success message | ||
var header bool | ||
|
||
if initArgs.FromModule != "" { | ||
src := initArgs.FromModule | ||
|
||
empty, err := configs.IsEmptyDir(path, initArgs.TestsDirectory) | ||
if err != nil { | ||
diags = diags.Append(fmt.Errorf("Error validating destination directory: %s", err)) | ||
view.Diagnostics(diags) | ||
return 1 | ||
} | ||
if !empty { | ||
diags = diags.Append(errors.New(strings.TrimSpace(errInitCopyNotEmpty))) | ||
view.Diagnostics(diags) | ||
return 1 | ||
} | ||
|
||
view.Output(views.CopyingConfigurationMessage, src) | ||
header = true | ||
|
||
hooks := uiModuleInstallHooks{ | ||
Ui: c.Ui, | ||
ShowLocalPaths: false, // since they are in a weird location for init | ||
View: view, | ||
} | ||
|
||
ctx, span := tracer.Start(ctx, "-from-module=...", trace.WithAttributes( | ||
attribute.String("module_source", src), | ||
)) | ||
|
||
initDirFromModuleAbort, initDirFromModuleDiags := c.initDirFromModule(ctx, path, src, hooks) | ||
diags = diags.Append(initDirFromModuleDiags) | ||
if initDirFromModuleAbort || initDirFromModuleDiags.HasErrors() { | ||
view.Diagnostics(diags) | ||
span.SetStatus(codes.Error, "module installation failed") | ||
span.End() | ||
return 1 | ||
} | ||
span.End() | ||
|
||
view.Output(views.EmptyMessage) | ||
} | ||
|
||
// If our directory is empty, then we're done. We can't get or set up | ||
// the backend with an empty directory. | ||
empty, err := configs.IsEmptyDir(path, initArgs.TestsDirectory) | ||
if err != nil { | ||
diags = diags.Append(fmt.Errorf("Error checking configuration: %s", err)) | ||
view.Diagnostics(diags) | ||
return 1 | ||
} | ||
if empty { | ||
view.Output(views.OutputInitEmptyMessage) | ||
return 0 | ||
} | ||
|
||
// Load just the root module to begin backend and module initialization | ||
rootModEarly, earlyConfDiags := c.loadSingleModuleWithTests(path, initArgs.TestsDirectory) | ||
|
||
// There may be parsing errors in config loading but these will be shown later _after_ | ||
// checking for core version requirement errors. Not meeting the version requirement should | ||
// be the first error displayed if that is an issue, but other operations are required | ||
// before being able to check core version requirements. | ||
if rootModEarly == nil { | ||
diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError)), earlyConfDiags) | ||
view.Diagnostics(diags) | ||
|
||
return 1 | ||
} | ||
|
||
var back backend.Backend | ||
|
||
// There may be config errors or backend init errors but these will be shown later _after_ | ||
// checking for core version requirement errors. | ||
var backDiags tfdiags.Diagnostics | ||
var backendOutput bool | ||
|
||
switch { | ||
case initArgs.Cloud && rootModEarly.CloudConfig != nil: | ||
back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view) | ||
case initArgs.Backend: | ||
back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view) | ||
default: | ||
// load the previously-stored backend config | ||
back, backDiags = c.Meta.backendFromState(ctx) | ||
} | ||
if backendOutput { | ||
header = true | ||
} | ||
|
||
var state *states.State | ||
|
||
// If we have a functional backend (either just initialized or initialized | ||
// on a previous run) we'll use the current state as a potential source | ||
// of provider dependencies. | ||
if back != nil { | ||
c.ignoreRemoteVersionConflict(back) | ||
workspace, err := c.Workspace() | ||
if err != nil { | ||
diags = diags.Append(fmt.Errorf("Error selecting workspace: %s", err)) | ||
view.Diagnostics(diags) | ||
return 1 | ||
} | ||
sMgr, err := back.StateMgr(workspace) | ||
if err != nil { | ||
diags = diags.Append(fmt.Errorf("Error loading state: %s", err)) | ||
view.Diagnostics(diags) | ||
return 1 | ||
} | ||
|
||
if err := sMgr.RefreshState(); err != nil { | ||
diags = diags.Append(fmt.Errorf("Error refreshing state: %s", err)) | ||
view.Diagnostics(diags) | ||
return 1 | ||
} | ||
|
||
state = sMgr.State() | ||
} | ||
|
||
if initArgs.Get { | ||
modsOutput, modsAbort, modsDiags := c.getModules(ctx, path, initArgs.TestsDirectory, rootModEarly, initArgs.Upgrade, view) | ||
diags = diags.Append(modsDiags) | ||
if modsAbort || modsDiags.HasErrors() { | ||
view.Diagnostics(diags) | ||
return 1 | ||
} | ||
if modsOutput { | ||
header = true | ||
} | ||
} | ||
|
||
// With all of the modules (hopefully) installed, we can now try to load the | ||
// whole configuration tree. | ||
config, confDiags := c.loadConfigWithTests(path, initArgs.TestsDirectory) | ||
// configDiags will be handled after the version constraint check, since an | ||
// incorrect version of terraform may be producing errors for configuration | ||
// constructs added in later versions. | ||
|
||
// Before we go further, we'll check to make sure none of the modules in | ||
// the configuration declare that they don't support this Terraform | ||
// version, so we can produce a version-related error message rather than | ||
// potentially-confusing downstream errors. | ||
versionDiags := terraform.CheckCoreVersionRequirements(config) | ||
if versionDiags.HasErrors() { | ||
view.Diagnostics(versionDiags) | ||
return 1 | ||
} | ||
|
||
// We've passed the core version check, now we can show errors from the | ||
// configuration and backend initialisation. | ||
|
||
// Now, we can check the diagnostics from the early configuration and the | ||
// backend. | ||
diags = diags.Append(earlyConfDiags) | ||
diags = diags.Append(backDiags) | ||
if earlyConfDiags.HasErrors() { | ||
diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError))) | ||
view.Diagnostics(diags) | ||
return 1 | ||
} | ||
|
||
// Now, we can show any errors from initializing the backend, but we won't | ||
// show the InitConfigError preamble as we didn't detect problems with | ||
// the early configuration. | ||
if backDiags.HasErrors() { | ||
view.Diagnostics(diags) | ||
return 1 | ||
} | ||
|
||
// If everything is ok with the core version check and backend initialization, | ||
// show other errors from loading the full configuration tree. | ||
diags = diags.Append(confDiags) | ||
if confDiags.HasErrors() { | ||
diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError))) | ||
view.Diagnostics(diags) | ||
return 1 | ||
} | ||
|
||
if cb, ok := back.(*cloud.Cloud); ok { | ||
if c.RunningInAutomation { | ||
if err := cb.AssertImportCompatible(config); err != nil { | ||
diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Compatibility error", err.Error())) | ||
view.Diagnostics(diags) | ||
return 1 | ||
} | ||
} | ||
} | ||
|
||
// Now that we have loaded all modules, check the module tree for missing providers. | ||
providersOutput, providersAbort, providerDiags := c.getProviders(ctx, config, state, initArgs.Upgrade, initArgs.PluginPath, initArgs.Lockfile, view) | ||
diags = diags.Append(providerDiags) | ||
if providersAbort || providerDiags.HasErrors() { | ||
view.Diagnostics(diags) | ||
return 1 | ||
} | ||
if providersOutput { | ||
header = true | ||
} | ||
|
||
// If we outputted information, then we need to output a newline | ||
// so that our success message is nicely spaced out from prior text. | ||
if header { | ||
view.Output(views.EmptyMessage) | ||
} | ||
|
||
// If we accumulated any warnings along the way that weren't accompanied | ||
// by errors then we'll output them here so that the success message is | ||
// still the final thing shown. | ||
view.Diagnostics(diags) | ||
_, cloud := back.(*cloud.Cloud) | ||
output := views.OutputInitSuccessMessage | ||
if cloud { | ||
output = views.OutputInitSuccessCloudMessage | ||
} | ||
|
||
view.Output(output) | ||
|
||
if !c.RunningInAutomation { | ||
// If we're not running in an automation wrapper, give the user | ||
// some more detailed next steps that are appropriate for interactive | ||
// shell usage. | ||
output = views.OutputInitSuccessCLIMessage | ||
if cloud { | ||
output = views.OutputInitSuccessCLICloudMessage | ||
} | ||
view.Output(output) | ||
// The else condition below invokes the original logic of the init command. | ||
// An experimental version of the init code will be used if: | ||
// > The user uses an experimental version of TF (alpha or built from source) | ||
// > The flag -enable-pss is passed to the init command. | ||
if c.Meta.AllowExperimentalFeatures && initArgs.EnablePssExperiment { | ||
// TODO(SarahFrench/radeksimko): Remove forked init logic once feature is no longer experimental | ||
panic("pss: experimental init code hasn't been added yet") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm fine with the panic here but I think we should make the message just tiny bit more user-friendly since we are exposing it to the user now. For example, how about Theoretically we could make it into a nice error but again - I'm fine with panic. 😄 |
||
} else { | ||
return c.run(initArgs, view) | ||
} | ||
return 0 | ||
} | ||
|
||
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) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not too fussy about what we call it internally but I'm thinking the public-facing flag could be a bit clearer and longer, e.g.
enable-pluggable-state-store-experiment
?I know, very (!) mouthful 😅 but the benefit is that it exactly makes people think twice about why they are enabling it and make it very obvious this is not a regular flag.