From ea8d0869d8ee55db1743979941d941523fb770b8 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Fri, 5 Apr 2024 01:30:55 -0400 Subject: [PATCH] convert all logs to be view type exclusive for human or json format --- internal/command/arguments/init.go | 116 ++++++++ internal/command/hook_module_install.go | 21 +- internal/command/init.go | 222 ++++++---------- internal/command/views/init.go | 340 ++++++++++++++++++++++++ 4 files changed, 551 insertions(+), 148 deletions(-) create mode 100644 internal/command/arguments/init.go diff --git a/internal/command/arguments/init.go b/internal/command/arguments/init.go new file mode 100644 index 0000000000..fc4a702769 --- /dev/null +++ b/internal/command/arguments/init.go @@ -0,0 +1,116 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "flag" + "time" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Init represents the command-line arguments for the init command. +type Init struct { + // FromModule identifies the module to copy into the target directory before init. + FromModule string + + // Lockfile specifies a dependency lockfile mode. + Lockfile string + + // TestDirectory is the directory containing any test files that should be + // validated alongside the main configuration. Should be relative to the + // Path. + TestsDirectory string + + // ViewType specifies which init format to use: human or JSON. + ViewType ViewType + + // Backend specifies whether to disable backend or Terraform Cloud initialization. + Backend bool + + // Cloud specifies whether to disable backend or Terraform Cloud initialization. + Cloud bool + + // Get specifies whether to disable downloading modules for this configuration + Get bool + + // ForceInitCopy specifies whether to suppress prompts about copying state data. + ForceInitCopy bool + + // StateLock specifies whether hold a state lock during backend migration. + StateLock bool + + // StateLockTimeout specifies the duration to wait for a state lock. + StateLockTimeout time.Duration + + // Reconfigure specifies whether to disregard any existing configuration, preventing migration of any existing state + Reconfigure bool + + // MigrateState specifies whether to attempt to copy existing state to the new backend + MigrateState bool + + // Upgrade specifies whether to upgrade modules and plugins as part of their respective installation steps + Upgrade bool + + // Json specifies whether to output in JSON format + Json bool + + // IgnoreRemoteVersion specifies whether to ignore remote and local Terraform versions compatibility + IgnoreRemoteVersion bool +} + +// ParseInit processes CLI arguments, returning an Init value and errors. +// If errors are encountered, an Init value is still returned representing +// the best effort interpretation of the arguments. +func ParseInit(args []string, cmdFlags *flag.FlagSet) (*Init, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + init := &Init{} + + cmdFlags.BoolVar(&init.Backend, "backend", true, "") + cmdFlags.BoolVar(&init.Cloud, "cloud", true, "") + cmdFlags.StringVar(&init.FromModule, "from-module", "", "copy the source of the given module into the directory before init") + cmdFlags.BoolVar(&init.Get, "get", true, "") + cmdFlags.BoolVar(&init.ForceInitCopy, "force-copy", false, "suppress prompts about copying state data") + cmdFlags.BoolVar(&init.StateLock, "lock", true, "lock state") + cmdFlags.DurationVar(&init.StateLockTimeout, "lock-timeout", 0, "lock timeout") + cmdFlags.BoolVar(&init.Reconfigure, "reconfigure", false, "reconfigure") + cmdFlags.BoolVar(&init.MigrateState, "migrate-state", false, "migrate state") + cmdFlags.BoolVar(&init.Upgrade, "upgrade", false, "") + cmdFlags.StringVar(&init.Lockfile, "lockfile", "", "Set a dependency lockfile mode") + cmdFlags.BoolVar(&init.IgnoreRemoteVersion, "ignore-remote-version", false, "continue even if remote and local Terraform versions are incompatible") + cmdFlags.StringVar(&init.TestsDirectory, "test-directory", "tests", "test-directory") + cmdFlags.BoolVar(&init.Json, "json", false, "json") + + if err := cmdFlags.Parse(args); err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + err.Error(), + )) + } + + backendFlagSet := FlagIsSet(cmdFlags, "backend") + cloudFlagSet := FlagIsSet(cmdFlags, "cloud") + + if backendFlagSet && cloudFlagSet { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid init options", + "The -backend and -cloud options are aliases of one another and mutually-exclusive in their use", + )) + } else if backendFlagSet { + init.Cloud = init.Backend + } else if cloudFlagSet { + init.Backend = init.Cloud + } + + switch { + case init.Json: + init.ViewType = ViewJSON + default: + init.ViewType = ViewHuman + } + + return init, diags +} diff --git a/internal/command/hook_module_install.go b/internal/command/hook_module_install.go index a795ac6380..9971cfbd32 100644 --- a/internal/command/hook_module_install.go +++ b/internal/command/hook_module_install.go @@ -11,26 +11,39 @@ import ( "github.com/hashicorp/terraform/internal/initwd" ) +type view interface { + Log(message string, params ...any) +} type uiModuleInstallHooks struct { initwd.ModuleInstallHooksImpl Ui cli.Ui ShowLocalPaths bool + View view } var _ initwd.ModuleInstallHooks = uiModuleInstallHooks{} func (h uiModuleInstallHooks) Download(modulePath, packageAddr string, v *version.Version) { if v != nil { - h.Ui.Info(fmt.Sprintf("Downloading %s %s for %s...", packageAddr, v, modulePath)) + h.log(fmt.Sprintf("Downloading %s %s for %s...", packageAddr, v, modulePath)) } else { - h.Ui.Info(fmt.Sprintf("Downloading %s for %s...", packageAddr, modulePath)) + h.log(fmt.Sprintf("Downloading %s for %s...", packageAddr, modulePath)) } } func (h uiModuleInstallHooks) Install(modulePath string, v *version.Version, localDir string) { if h.ShowLocalPaths { - h.Ui.Info(fmt.Sprintf("- %s in %s", modulePath, localDir)) + h.log(fmt.Sprintf("- %s in %s", modulePath, localDir)) } else { - h.Ui.Info(fmt.Sprintf("- %s", modulePath)) + h.log(fmt.Sprintf("- %s", modulePath)) + } +} + +func (h uiModuleInstallHooks) log(message string) { + switch h.View.(type) { + case view: + h.View.Log(message) + default: + h.Ui.Info(message) } } diff --git a/internal/command/init.go b/internal/command/init.go index 4993c2d982..a869503ce8 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -43,50 +43,47 @@ type InitCommand struct { } func (c *InitCommand) Run(args []string) int { - var flagFromModule, flagLockfile, testsDirectory string - var flagBackend, flagCloud, flagGet, flagUpgrade, flagJson bool var flagPluginPath FlagStringSlice flagConfigExtra := newRawFlags("-backend-config") + var diags tfdiags.Diagnostics args = c.Meta.process(args) cmdFlags := c.Meta.extendedFlagSet("init") - cmdFlags.BoolVar(&flagBackend, "backend", true, "") - cmdFlags.BoolVar(&flagCloud, "cloud", true, "") + cmdFlags.Usage = func() { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + c.Help(), + )) + } + cmdFlags.Var(flagConfigExtra, "backend-config", "") - cmdFlags.StringVar(&flagFromModule, "from-module", "", "copy the source of the given module into the directory before init") - cmdFlags.BoolVar(&flagGet, "get", true, "") - cmdFlags.BoolVar(&c.forceInitCopy, "force-copy", false, "suppress prompts about copying state data") - cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state") - cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout") - cmdFlags.BoolVar(&c.reconfigure, "reconfigure", false, "reconfigure") - cmdFlags.BoolVar(&c.migrateState, "migrate-state", false, "migrate state") - cmdFlags.BoolVar(&flagUpgrade, "upgrade", false, "") cmdFlags.Var(&flagPluginPath, "plugin-dir", "plugin directory") - cmdFlags.StringVar(&flagLockfile, "lockfile", "", "Set a dependency lockfile mode") - cmdFlags.BoolVar(&c.Meta.ignoreRemoteVersion, "ignore-remote-version", false, "continue even if remote and local Terraform versions are incompatible") - cmdFlags.StringVar(&testsDirectory, "test-directory", "tests", "test-directory") - cmdFlags.BoolVar(&flagJson, "json", false, "json") - cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } - if err := cmdFlags.Parse(args); err != nil { - return 1 - } + initArgs, initDiags := arguments.ParseInit(args, cmdFlags) - backendFlagSet := arguments.FlagIsSet(cmdFlags, "backend") - cloudFlagSet := arguments.FlagIsSet(cmdFlags, "cloud") + view := views.NewInit(initArgs.ViewType, c.View) - switch { - case backendFlagSet && cloudFlagSet: - c.Ui.Error("The -backend and -cloud options are aliases of one another and mutually-exclusive in their use") + if initDiags.HasErrors() { + diags = diags.Append(initDiags) + view.Diagnostics(diags) return 1 - case backendFlagSet: - flagCloud = flagBackend - case cloudFlagSet: - flagBackend = flagCloud } + 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 + if c.migrateState && c.reconfigure { - c.Ui.Error("The -migrate-state and -reconfigure options are mutually-exclusive") + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid init options", + "The -migrate-state and -reconfigure options are mutually-exclusive", + )) + view.Diagnostics(diags) return 1 } @@ -96,17 +93,6 @@ func (c *InitCommand) Run(args []string) int { c.migrateState = true } - var viewType arguments.ViewType - switch { - case flagJson: - viewType = arguments.ViewJSON - default: - viewType = arguments.ViewHuman - } - view := views.NewInit(viewType, c.View) - - var diags tfdiags.Diagnostics - if len(flagPluginPath) > 0 { c.pluginPath = flagPluginPath } @@ -134,8 +120,8 @@ func (c *InitCommand) Run(args []string) int { // to output a newline before the success message var header bool - if flagFromModule != "" { - src := flagFromModule + if initArgs.FromModule != "" { + src := initArgs.FromModule empty, err := configs.IsEmptyDir(path) if err != nil { @@ -149,14 +135,13 @@ func (c *InitCommand) Run(args []string) int { return 1 } - c.Ui.Output(c.Colorize().Color(fmt.Sprintf( - "[reset][bold]Copying configuration[reset] from %q...", src, - ))) + view.Output(views.CopyingConfigurationMessage, src) header = true - hooks := uiModuleInstallHooks{ + hooks := uiModuleInstallHooks{ // here check to verify if downloading prints text, update to handle view type 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( @@ -173,7 +158,7 @@ func (c *InitCommand) Run(args []string) int { } span.End() - c.Ui.Output("") + view.Output(views.EmptyMessage) } // If our directory is empty, then we're done. We can't get or set up @@ -185,20 +170,19 @@ func (c *InitCommand) Run(args []string) int { return 1 } if empty { - c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitEmpty))) + view.Output(views.OutputInitEmptyMessage) return 0 } // Load just the root module to begin backend and module initialization - rootModEarly, earlyConfDiags := c.loadSingleModuleWithTests(path, testsDirectory) + 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 { - c.Ui.Error(c.Colorize().Color(strings.TrimSpace(errInitConfigError))) - diags = diags.Append(earlyConfDiags) + diags = diags.Append(fmt.Errorf(view.PrepareMessage(views.InitConfigError)), earlyConfDiags) view.Diagnostics(diags) return 1 @@ -212,10 +196,10 @@ func (c *InitCommand) Run(args []string) int { var backendOutput bool switch { - case flagCloud && rootModEarly.CloudConfig != nil: - back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, flagConfigExtra, viewType) - case flagBackend: - back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, flagConfigExtra, viewType) + case initArgs.Cloud && rootModEarly.CloudConfig != nil: + back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, flagConfigExtra, initArgs.ViewType, view) + case initArgs.Backend: + back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, flagConfigExtra, initArgs.ViewType, view) default: // load the previously-stored backend config back, backDiags = c.Meta.backendFromState(ctx) @@ -253,8 +237,8 @@ func (c *InitCommand) Run(args []string) int { state = sMgr.State() } - if flagGet { - modsOutput, modsAbort, modsDiags := c.getModules(ctx, path, testsDirectory, rootModEarly, flagUpgrade) + 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) @@ -267,7 +251,7 @@ func (c *InitCommand) Run(args []string) int { // With all of the modules (hopefully) installed, we can now try to load the // whole configuration tree. - config, confDiags := c.loadConfigWithTests(path, testsDirectory) + 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. @@ -290,13 +274,13 @@ func (c *InitCommand) Run(args []string) int { diags = diags.Append(earlyConfDiags) diags = diags.Append(backDiags) if earlyConfDiags.HasErrors() { - c.Ui.Error(strings.TrimSpace(errInitConfigError)) + diags = diags.Append(fmt.Errorf(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 errInitConfigError preamble as we didn't detect problems with + // show the InitConfigError preamble as we didn't detect problems with // the early configuration. if backDiags.HasErrors() { view.Diagnostics(diags) @@ -307,7 +291,7 @@ func (c *InitCommand) Run(args []string) int { // show other errors from loading the full configuration tree. diags = diags.Append(confDiags) if confDiags.HasErrors() { - c.Ui.Error(strings.TrimSpace(errInitConfigError)) + diags = diags.Append(fmt.Errorf(view.PrepareMessage(views.InitConfigError))) view.Diagnostics(diags) return 1 } @@ -323,7 +307,7 @@ func (c *InitCommand) Run(args []string) int { } // Now that we have loaded all modules, check the module tree for missing providers. - providersOutput, providersAbort, providerDiags := c.getProviders(ctx, config, state, flagUpgrade, flagPluginPath, flagLockfile, view) + providersOutput, providersAbort, providerDiags := c.getProviders(ctx, config, state, initArgs.Upgrade, flagPluginPath, initArgs.Lockfile, view) diags = diags.Append(providerDiags) if providersAbort || providerDiags.HasErrors() { view.Diagnostics(diags) @@ -336,7 +320,7 @@ func (c *InitCommand) Run(args []string) int { // 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 { - c.Ui.Output("") + view.Output(views.EmptyMessage) } // If we accumulated any warnings along the way that weren't accompanied @@ -344,27 +328,27 @@ func (c *InitCommand) Run(args []string) int { // still the final thing shown. view.Diagnostics(diags) _, cloud := back.(*cloud.Cloud) - output := outputInitSuccess + output := views.OutputInitSuccessMessage if cloud { - output = outputInitSuccessCloud + output = views.OutputInitSuccessCloudMessage } - c.Ui.Output(c.Colorize().Color(strings.TrimSpace(output))) + 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 = outputInitSuccessCLI + output = views.OutputInitSuccessCLIMessage if cloud { - output = outputInitSuccessCLICloud + output = views.OutputInitSuccessCLICloudMessage } - c.Ui.Output(c.Colorize().Color(strings.TrimSpace(output))) + view.Output(output) } return 0 } -func (c *InitCommand) getModules(ctx context.Context, path, testsDir string, earlyRoot *configs.Module, upgrade bool) (output bool, abort bool, diags tfdiags.Diagnostics) { +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) { testModules := false // We can also have modules buried in test files. for _, file := range earlyRoot.Tests { for _, run := range file.Runs { @@ -385,14 +369,15 @@ func (c *InitCommand) getModules(ctx context.Context, path, testsDir string, ear defer span.End() if upgrade { - c.Ui.Output(c.Colorize().Color("[reset][bold]Upgrading modules...")) + view.Output(views.UpgradingModulesMessage) } else { - c.Ui.Output(c.Colorize().Color("[reset][bold]Initializing modules...")) + view.Output(views.InitializingModulesMessage) } hooks := uiModuleInstallHooks{ Ui: c.Ui, ShowLocalPaths: true, + View: view, } installAbort, installDiags := c.installModules(ctx, path, testsDir, upgrade, false, hooks) @@ -418,12 +403,12 @@ func (c *InitCommand) getModules(ctx context.Context, path, testsDir string, ear return true, installAbort, diags } -func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extraConfig rawFlags, viewType arguments.ViewType) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { +func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extraConfig rawFlags, viewType arguments.ViewType, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { ctx, span := tracer.Start(ctx, "initialize Terraform Cloud") _ = ctx // prevent staticcheck from complaining to avoid a maintenence hazard of having the wrong ctx in scope here defer span.End() - c.Ui.Output(c.Colorize().Color("\n[reset][bold]Initializing Terraform Cloud...")) + view.Output(views.InitializingTerraformCloudMessage) if len(extraConfig.AllItems()) != 0 { diags = diags.Append(tfdiags.Sourceless( @@ -447,12 +432,12 @@ func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extra return back, true, diags } -func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, extraConfig rawFlags, viewType arguments.ViewType) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { +func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, extraConfig rawFlags, viewType arguments.ViewType, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { ctx, span := tracer.Start(ctx, "initialize backend") _ = ctx // prevent staticcheck from complaining to avoid a maintenence hazard of having the wrong ctx in scope here defer span.End() - c.Ui.Output(c.Colorize().Color("\n[reset][bold]Initializing the backend...")) + view.Output(views.InitializingBackendMessage) var backendConfig *configs.Backend var backendConfigOverride hcl.Body @@ -606,15 +591,13 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, // are shimming our vt100 output to the legacy console API on Windows. evts := &providercache.InstallerEvents{ PendingProviders: func(reqs map[addrs.Provider]getproviders.VersionConstraints) { - c.Ui.Output(c.Colorize().Color( - "\n[reset][bold]Initializing provider plugins...", - )) + view.Output(views.InitializingProviderPluginMessage) }, ProviderAlreadyInstalled: func(provider addrs.Provider, selectedVersion getproviders.Version) { - c.Ui.Info(fmt.Sprintf("- Using previously-installed %s v%s", provider.ForDisplay(), selectedVersion)) + view.Log(views.ProviderAlreadyInstalledMessage, provider.ForDisplay(), selectedVersion) }, BuiltInProviderAvailable: func(provider addrs.Provider) { - c.Ui.Info(fmt.Sprintf("- %s is built in to Terraform", provider.ForDisplay())) + view.Log(views.BuiltInProviderAvailableMessage, provider.ForDisplay()) }, BuiltInProviderFailure: func(provider addrs.Provider, err error) { diags = diags.Append(tfdiags.Sourceless( @@ -625,20 +608,20 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, }, QueryPackagesBegin: func(provider addrs.Provider, versionConstraints getproviders.VersionConstraints, locked bool) { if locked { - c.Ui.Info(fmt.Sprintf("- Reusing previous version of %s from the dependency lock file", provider.ForDisplay())) + view.Log(views.ReusingPreviousVersionInfo, provider.ForDisplay()) } else { if len(versionConstraints) > 0 { - c.Ui.Info(fmt.Sprintf("- Finding %s versions matching %q...", provider.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints))) + view.Log(views.FindingMatchingVersionMessage, provider.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints)) } else { - c.Ui.Info(fmt.Sprintf("- Finding latest version of %s...", provider.ForDisplay())) + view.Log(views.FindingLatestVersionMessage, provider.ForDisplay()) } } }, LinkFromCacheBegin: func(provider addrs.Provider, version getproviders.Version, cacheRoot string) { - c.Ui.Info(fmt.Sprintf("- Using %s v%s from the shared cache directory", provider.ForDisplay(), version)) + view.Log(views.UsingProviderFromCacheDirInfo, provider.ForDisplay(), version) }, FetchPackageBegin: func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation) { - c.Ui.Info(fmt.Sprintf("- Installing %s v%s...", provider.ForDisplay(), version)) + view.Log(views.InstallingProviderMessage, provider.ForDisplay(), version) }, QueryPackagesFailure: func(provider addrs.Provider, err error) { switch errorTy := err.(type) { @@ -835,10 +818,10 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, keyID = authResult.KeyID } if keyID != "" { - keyID = c.Colorize().Color(fmt.Sprintf(", key ID [reset][bold]%s[reset]", keyID)) + keyID = view.PrepareMessage(views.KeyID, keyID) } - c.Ui.Info(fmt.Sprintf("- Installed %s v%s (%s%s)", provider.ForDisplay(), version, authResult, keyID)) + view.Log(views.InstalledProviderVersionInfo, provider.ForDisplay(), version, authResult, keyID) }, ProvidersLockUpdated: func(provider addrs.Provider, version getproviders.Version, localHashes []getproviders.Hash, signedHashes []getproviders.Hash, priorHashes []getproviders.Hash) { // We're going to use this opportunity to track if we have any @@ -884,9 +867,7 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, } } if thirdPartySigned { - c.Ui.Info(fmt.Sprintf("\nPartner and community providers are signed by their developers.\n" + - "If you'd like to know more about provider signing, you can read about it here:\n" + - "https://www.terraform.io/docs/cli/plugins/signing.html")) + view.Log(views.PartnerAndCommunityProvidersMessage) } }, } @@ -895,7 +876,8 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, mode := providercache.InstallNewProvidersOnly if upgrade { if flagLockfile == "readonly" { - c.Ui.Error("The -upgrade flag conflicts with -lockfile=readonly.") + diags = diags.Append(fmt.Errorf("The -upgrade flag conflicts with -lockfile=readonly.")) + view.Diagnostics(diags) return true, true, diags } @@ -903,8 +885,8 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, } newLocks, err := inst.EnsureProviderVersions(ctx, previousLocks, reqs, mode) if ctx.Err() == context.Canceled { + diags = diags.Append(fmt.Errorf("Provider installation was canceled by an interrupt signal.")) view.Diagnostics(diags) - c.Ui.Error("Provider installation was canceled by an interrupt signal.") return true, true, diags } if err != nil { @@ -971,16 +953,9 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, // say a little about what the dependency lock file is, for new // users or those who are upgrading from a previous Terraform // version that didn't have dependency lock files. - c.Ui.Output(c.Colorize().Color(` -Terraform has created a lock file [bold].terraform.lock.hcl[reset] to record the provider -selections it made above. Include this file in your version control repository -so that Terraform can guarantee to make the same selections by default when -you run "terraform init" in the future.`)) + view.Output(views.LockInfo) } else { - c.Ui.Output(c.Colorize().Color(` -Terraform has made some changes to the provider dependency selections recorded -in the .terraform.lock.hcl file. Review those changes and commit them to your -version control system if they represent changes you intended to make.`)) + view.Output(views.DependenciesLockChangesInfo) } moreDiags = c.replaceLockedDependencies(newLocks) @@ -1213,14 +1188,6 @@ func (c *InitCommand) Synopsis() string { return "Prepare your working directory for other commands" } -const errInitConfigError = ` -[reset]Terraform encountered problems during initialisation, including problems -with the configuration, described below. - -The Terraform configuration must be valid before initialization so that -Terraform can determine which modules and providers need to be installed. -` - const errInitCopyNotEmpty = ` The working directory already contains files. The -from-module option requires an empty directory into which a copy of the referenced module will be placed. @@ -1229,39 +1196,6 @@ To initialize the configuration already in this working directory, omit the -from-module option. ` -const outputInitEmpty = ` -[reset][bold]Terraform initialized in an empty directory![reset] - -The directory has no Terraform configuration files. You may begin working -with Terraform immediately by creating Terraform configuration files. -` - -const outputInitSuccess = ` -[reset][bold][green]Terraform has been successfully initialized![reset][green] -` - -const outputInitSuccessCloud = ` -[reset][bold][green]Terraform Cloud has been successfully initialized![reset][green] -` - -const outputInitSuccessCLI = `[reset][green] -You may now begin working with Terraform. Try running "terraform plan" to see -any changes that are required for your infrastructure. All Terraform commands -should now work. - -If you ever set or change modules or backend configuration for Terraform, -rerun this command to reinitialize your working directory. If you forget, other -commands will detect it and remind you to do so if necessary. -` - -const outputInitSuccessCLICloud = `[reset][green] -You may now begin working with Terraform Cloud. Try running "terraform plan" to -see any changes that are required for your infrastructure. - -If you ever set or change modules or Terraform Settings, run "terraform init" -again to reinitialize your working directory. -` - // providerProtocolTooOld is a message sent to the CLI UI if the provider's // supported protocol versions are too old for the user's version of terraform, // but a newer version of the provider is compatible. diff --git a/internal/command/views/init.go b/internal/command/views/init.go index 80967a2820..a53a80a282 100644 --- a/internal/command/views/init.go +++ b/internal/command/views/init.go @@ -4,7 +4,10 @@ package views import ( + "encoding/json" "fmt" + "strings" + "time" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/tfdiags" @@ -13,6 +16,9 @@ import ( // The Init view is used for the init command. type Init interface { Diagnostics(diags tfdiags.Diagnostics) + Output(messageCode string, params ...any) + Log(messageCode string, params ...any) + PrepareMessage(messageCode string, params ...any) string } // NewInit returns Init implementation for the given ViewType. @@ -43,6 +49,29 @@ func (v *InitHuman) Diagnostics(diags tfdiags.Diagnostics) { v.view.Diagnostics(diags) } +func (v *InitHuman) Output(messageCode string, params ...any) { + v.view.streams.Println(v.PrepareMessage(messageCode, params...)) +} + +func (v *InitHuman) Log(messageCode string, params ...any) { + v.view.streams.Println(v.PrepareMessage(messageCode, params...)) +} + +func (v *InitHuman) PrepareMessage(messageCode string, params ...any) string { + message, ok := MessageRegistry[messageCode] + if !ok { + // display the message code as fallback if not found in the message registry + return messageCode + } + + if message.HumanValue == "" { + // no need to apply colorization if the message is empty + return message.HumanValue + } + + return v.view.colorize.Color(strings.TrimSpace(fmt.Sprintf(message.HumanValue, params...))) +} + // The InitJSON implementation renders streaming JSON logs, suitable for // integrating with other software. type InitJSON struct { @@ -54,3 +83,314 @@ var _ Init = (*InitJSON)(nil) func (v *InitJSON) Diagnostics(diags tfdiags.Diagnostics) { v.view.Diagnostics(diags) } + +func (v *InitJSON) Output(messageCode string, params ...any) { + current_timestamp := time.Now().Format(time.RFC3339) + + json_data := map[string]string{ + "@level": "info", + "@message": v.PrepareMessage(messageCode, params...), + "@module": "terraform.ui", + "@timestamp": current_timestamp, + "type": "init_output"} + + init_output, _ := json.Marshal(json_data) + v.view.view.streams.Println(string(init_output)) +} + +func (v *InitJSON) Log(messageCode string, params ...any) { + v.view.Log(v.PrepareMessage(messageCode, params...)) +} + +func (v *InitJSON) PrepareMessage(messageCode string, params ...any) string { + message, ok := MessageRegistry[messageCode] + if !ok { + // display the message code as fallback if not found in the message registry + return messageCode + } + + return strings.TrimSpace(fmt.Sprintf(message.JSONValue, params...)) +} + +// InitMessage represents a message string in both json and human decorated text format. +type InitMessage struct { + HumanValue string + JSONValue string +} + +var MessageRegistry map[string]InitMessage = map[string]InitMessage{ + "copying_configuration_message": { + HumanValue: "[reset][bold]Copying configuration[reset] from %q...", + JSONValue: "Copying configuration from %q...", + }, + "output_init_empty_message": { + HumanValue: outputInitEmpty, + JSONValue: outputInitEmptyJSON, + }, + "output_init_success_message": { + HumanValue: outputInitSuccess, + JSONValue: outputInitSuccessJSON, + }, + "output_init_success_cloud_message": { + HumanValue: outputInitSuccessCloud, + JSONValue: outputInitSuccessCloudJSON, + }, + "output_init_success_cli_message": { + HumanValue: outputInitSuccessCLI, + JSONValue: outputInitSuccessCLI_JSON, + }, + "output_init_success_cli_cloud_message": { + HumanValue: outputInitSuccessCLICloud, + JSONValue: outputInitSuccessCLICloudJSON, + }, + "upgrading_modules_message": { + HumanValue: "[reset][bold]Upgrading modules...", + JSONValue: "Upgrading modules...", + }, + "initializing_modules_message": { + HumanValue: "[reset][bold]Initializing modules...", + JSONValue: "Initializing modules...", + }, + "initializing_terraform_cloud_message": { + HumanValue: "\n[reset][bold]Initializing Terraform Cloud...", + JSONValue: "Initializing Terraform Cloud...", + }, + "initializing_backend_message": { + HumanValue: "\n[reset][bold]Initializing the backend...", + JSONValue: "Initializing the backend...", + }, + "initializing_provider_plugin_message": { + HumanValue: "\n[reset][bold]Initializing provider plugins...", + JSONValue: "Initializing provider plugins...", + }, + "dependencies_lock_changes_info": { + HumanValue: dependenciesLockChangesInfo, + JSONValue: dependenciesLockChangesInfo, + }, + "lock_info": { + HumanValue: previousLockInfoHuman, + JSONValue: previousLockInfoJSON, + }, + "provider_already_installed_message": { + HumanValue: "- Using previously-installed %s v%s", + JSONValue: "- Using previously-installed %s v%s", + }, + "built_in_provider_available_message": { + HumanValue: "- %s is built in to Terraform", + JSONValue: "- %s is built in to Terraform", + }, + "reusing_previous_version_info": { + HumanValue: "- Reusing previous version of %s from the dependency lock file", + JSONValue: "- Reusing previous version of %s from the dependency lock file", + }, + "finding_matching_version_message": { + HumanValue: "- Finding %s versions matching %q...", + JSONValue: "- Finding %s versions matching %q...", + }, + "finding_latest_version_message": { + HumanValue: "- Finding latest version of %s...", + JSONValue: "- Finding latest version of %s...", + }, + "using_provider_from_cache_dir_info": { + HumanValue: "- Using %s v%s from the shared cache directory", + JSONValue: "- Using %s v%s from the shared cache directory", + }, + "installing_provider_message": { + HumanValue: "- Installing %s v%s...", + JSONValue: "- Installing %s v%s...", + }, + "key_id": { + HumanValue: ", key ID [reset][bold]%s[reset]", + JSONValue: ", key ID %s", + }, + "installed_provider_version_info": { + HumanValue: "- Installed %s v%s (%s%s)", + JSONValue: "- Installed %s v%s (%s%s)", + }, + "partner_and_community_providers_message": { + HumanValue: partnerAndCommunityProvidersInfo, + JSONValue: partnerAndCommunityProvidersInfo, + }, + "init_config_error": { + HumanValue: errInitConfigError, + JSONValue: errInitConfigErrorJSON, + }, + "empty_message": { + HumanValue: "", + JSONValue: "", + }, +} + +const ( + CopyingConfigurationMessage string = "copying_configuration_message" + EmptyMessage string = "empty_message" + OutputInitEmptyMessage string = "output_init_empty_message" + OutputInitSuccessMessage string = "output_init_success_message" + OutputInitSuccessCloudMessage string = "output_init_success_cloud_message" + OutputInitSuccessCLIMessage string = "output_init_success_cli_message" + OutputInitSuccessCLICloudMessage string = "output_init_success_cli_cloud_message" + UpgradingModulesMessage string = "upgrading_modules_message" + InitializingTerraformCloudMessage string = "initializing_terraform_cloud_message" + InitializingModulesMessage string = "initializing_modules_message" + InitializingBackendMessage string = "initializing_backend_message" + InitializingProviderPluginMessage string = "initializing_provider_plugin_message" + LockInfo string = "lock_info" + DependenciesLockChangesInfo string = "dependencies_lock_changes_info" + ProviderAlreadyInstalledMessage string = "provider_already_installed_message" + BuiltInProviderAvailableMessage string = "built_in_provider_available_message" + ReusingPreviousVersionInfo string = "reusing_previous_version_info" + FindingMatchingVersionMessage string = "finding_matching_version_message" + FindingLatestVersionMessage string = "finding_latest_version_message" + UsingProviderFromCacheDirInfo string = "using_provider_from_cache_dir_info" + InstallingProviderMessage string = "installing_provider_message" + KeyID string = "key_id" + InstalledProviderVersionInfo string = "installed_provider_version_info" + PartnerAndCommunityProvidersMessage string = "partner_and_community_providers_message" + InitConfigError string = "init_config_error" +) + +const outputInitEmpty = ` +[reset][bold]Terraform initialized in an empty directory![reset] + +The directory has no Terraform configuration files. You may begin working +with Terraform immediately by creating Terraform configuration files. +` + +const outputInitEmptyJSON = ` +Terraform initialized in an empty directory! + +The directory has no Terraform configuration files. You may begin working +with Terraform immediately by creating Terraform configuration files. +` + +const outputInitSuccess = ` +[reset][bold][green]Terraform has been successfully initialized![reset][green] +` + +const outputInitSuccessJSON = ` +Terraform has been successfully initialized! +` + +const outputInitSuccessCloud = ` +[reset][bold][green]Terraform Cloud has been successfully initialized![reset][green] +` + +const outputInitSuccessCloudJSON = ` +Terraform Cloud has been successfully initialized! +` + +const outputInitSuccessCLI = `[reset][green] +You may now begin working with Terraform. Try running "terraform plan" to see +any changes that are required for your infrastructure. All Terraform commands +should now work. + +If you ever set or change modules or backend configuration for Terraform, +rerun this command to reinitialize your working directory. If you forget, other +commands will detect it and remind you to do so if necessary. +` + +const outputInitSuccessCLI_JSON = ` +You may now begin working with Terraform. Try running "terraform plan" to see +any changes that are required for your infrastructure. All Terraform commands +should now work. + +If you ever set or change modules or backend configuration for Terraform, +rerun this command to reinitialize your working directory. If you forget, other +commands will detect it and remind you to do so if necessary. +` + +const outputInitSuccessCLICloud = `[reset][green] +You may now begin working with Terraform Cloud. Try running "terraform plan" to +see any changes that are required for your infrastructure. + +If you ever set or change modules or Terraform Settings, run "terraform init" +again to reinitialize your working directory. +` + +const outputInitSuccessCLICloudJSON = ` +You may now begin working with Terraform Cloud. Try running "terraform plan" to +see any changes that are required for your infrastructure. + +If you ever set or change modules or Terraform Settings, run "terraform init" +again to reinitialize your working directory. +` + +// providerProtocolTooOld is a message sent to the CLI UI if the provider's +// supported protocol versions are too old for the user's version of terraform, +// but a newer version of the provider is compatible. +const providerProtocolTooOld = `Provider %q v%s is not compatible with Terraform %s. +Provider version %s is the latest compatible version. Select it with the following version constraint: + version = %q + +Terraform checked all of the plugin versions matching the given constraint: + %s + +Consult the documentation for this provider for more information on compatibility between provider and Terraform versions. +` + +// providerProtocolTooNew is a message sent to the CLI UI if the provider's +// supported protocol versions are too new for the user's version of terraform, +// and the user could either upgrade terraform or choose an older version of the +// provider. +const providerProtocolTooNew = `Provider %q v%s is not compatible with Terraform %s. +You need to downgrade to v%s or earlier. Select it with the following constraint: + version = %q + +Terraform checked all of the plugin versions matching the given constraint: + %s + +Consult the documentation for this provider for more information on compatibility between provider and Terraform versions. +Alternatively, upgrade to the latest version of Terraform for compatibility with newer provider releases. +` + +// incompleteLockFileInformationHeader is the summary displayed to users when +// the lock file has only recorded local hashes. +const incompleteLockFileInformationHeader = `Incomplete lock file information for providers` + +// incompleteLockFileInformationBody is the body of text displayed to users when +// the lock file has only recorded local hashes. +const incompleteLockFileInformationBody = `Due to your customized provider installation methods, Terraform was forced to calculate lock file checksums locally for the following providers: + - %s + +The current .terraform.lock.hcl file only includes checksums for %s, so Terraform running on another platform will fail to install these providers. + +To calculate additional checksums for another platform, run: + terraform providers lock -platform=linux_amd64 +(where linux_amd64 is the platform to generate)` + +const previousLockInfoHuman = ` +Terraform has created a lock file [bold].terraform.lock.hcl[reset] to record the provider +selections it made above. Include this file in your version control repository +so that Terraform can guarantee to make the same selections by default when +you run "terraform init" in the future.` + +const previousLockInfoJSON = ` +Terraform has created a lock file .terraform.lock.hcl to record the provider +selections it made above. Include this file in your version control repository +so that Terraform can guarantee to make the same selections by default when +you run "terraform init" in the future.` + +const dependenciesLockChangesInfo = ` +Terraform has made some changes to the provider dependency selections recorded +in the .terraform.lock.hcl file. Review those changes and commit them to your +version control system if they represent changes you intended to make.` + +const partnerAndCommunityProvidersInfo = "\nPartner and community providers are signed by their developers.\n" + + "If you'd like to know more about provider signing, you can read about it here:\n" + + "https://www.terraform.io/docs/cli/plugins/signing.html" + +const errInitConfigError = ` +[reset]Terraform encountered problems during initialisation, including problems +with the configuration, described below. + +The Terraform configuration must be valid before initialization so that +Terraform can determine which modules and providers need to be installed. +` + +const errInitConfigErrorJSON = ` +Terraform encountered problems during initialisation, including problems +with the configuration, described below. + +The Terraform configuration must be valid before initialization so that +Terraform can determine which modules and providers need to be installed. +`