From 14c53f74f266371e04e57d58443ccbd7d3bb2802 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Tue, 26 Mar 2024 03:57:59 -0400 Subject: [PATCH 01/13] add init view for json and human type view --- internal/command/views/init.go | 56 ++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 internal/command/views/init.go diff --git a/internal/command/views/init.go b/internal/command/views/init.go new file mode 100644 index 000000000000..80967a282000 --- /dev/null +++ b/internal/command/views/init.go @@ -0,0 +1,56 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package views + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/command/arguments" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// The Init view is used for the init command. +type Init interface { + Diagnostics(diags tfdiags.Diagnostics) +} + +// NewInit returns Init implementation for the given ViewType. +func NewInit(vt arguments.ViewType, view *View) Init { + switch vt { + case arguments.ViewJSON: + return &InitJSON{ + view: NewJSONView(view), + } + case arguments.ViewHuman: + return &InitHuman{ + view: view, + } + default: + panic(fmt.Sprintf("unknown view type %v", vt)) + } +} + +// The InitHuman implementation renders human-readable text logs, suitable for +// a scrolling terminal. +type InitHuman struct { + view *View +} + +var _ Init = (*InitHuman)(nil) + +func (v *InitHuman) Diagnostics(diags tfdiags.Diagnostics) { + v.view.Diagnostics(diags) +} + +// The InitJSON implementation renders streaming JSON logs, suitable for +// integrating with other software. +type InitJSON struct { + view *JSONView +} + +var _ Init = (*InitJSON)(nil) + +func (v *InitJSON) Diagnostics(diags tfdiags.Diagnostics) { + v.view.Diagnostics(diags) +} From 7709db45dcde5a27c650cd43514e606d1d35e028 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Tue, 26 Mar 2024 03:58:34 -0400 Subject: [PATCH 02/13] add init view test --- internal/command/views/init_test.go | 120 ++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 internal/command/views/init_test.go diff --git a/internal/command/views/init_test.go b/internal/command/views/init_test.go new file mode 100644 index 000000000000..6b293ed97aa9 --- /dev/null +++ b/internal/command/views/init_test.go @@ -0,0 +1,120 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package views + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/command/arguments" + "github.com/hashicorp/terraform/internal/terminal" + "github.com/hashicorp/terraform/internal/tfdiags" + tfversion "github.com/hashicorp/terraform/version" +) + +func TestNewInit_jsonView(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitJSON); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + diags := getTestDiags(t) + newInit.Diagnostics(diags) + + version := tfversion.String() + want := []map[string]interface{}{ + { + "@level": "info", + "@message": fmt.Sprintf("Terraform %s", version), + "@module": "terraform.ui", + "terraform": version, + "type": "version", + "ui": JSON_UI_VERSION, + }, + { + "@level": "error", + "@message": "Error: Error selecting workspace", + "@module": "terraform.ui", + "diagnostic": map[string]interface{}{ + "severity": "error", + "summary": "Error selecting workspace", + "detail": "Workspace random_pet does not exist", + }, + "type": "diagnostic", + }, + { + "@level": "error", + "@message": "Error: Unsupported backend type", + "@module": "terraform.ui", + "diagnostic": map[string]interface{}{ + "severity": "error", + "summary": "Unsupported backend type", + "detail": "There is no explicit backend type named fake backend.", + }, + "type": "diagnostic", + }, + } + + actual := done(t).Stdout() + testJSONViewOutputEqualsFull(t, actual, want) +} + +func TestNewInit_humanView(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewHuman, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitHuman); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + diags := getTestDiags(t) + newInit.Diagnostics(diags) + + actual := done(t).All() + expected := "\nError: Error selecting workspace\n\nWorkspace random_pet does not exist\n\nError: Unsupported backend type\n\nThere is no explicit backend type named fake backend.\n" + if !strings.Contains(actual, expected) { + t.Fatalf("expected output to contain: %s, but got %s", expected, actual) + } +} + +func TestNewInit_unsupportedView(t *testing.T) { + defer func() { + r := recover() + if r == nil { + t.Fatalf("should panic with unsupported view type raw") + } else if r != "unknown view type raw" { + t.Fatalf("unexpected panic message: %v", r) + } + }() + + streams, done := terminal.StreamsForTesting(t) + defer done(t) + + NewInit(arguments.ViewRaw, NewView(streams).SetRunningInAutomation(true)) +} + +func getTestDiags(t *testing.T) tfdiags.Diagnostics { + t.Helper() + + var diags tfdiags.Diagnostics + diags = diags.Append( + tfdiags.Sourceless( + tfdiags.Error, + "Error selecting workspace", + "Workspace random_pet does not exist", + ), + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unsupported backend type", + Detail: "There is no explicit backend type named fake backend.", + Subject: nil, + }, + ) + + return diags +} From a3f3b64e4ce6d50011498c9bc607592eb90edcd7 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Tue, 26 Mar 2024 04:00:52 -0400 Subject: [PATCH 03/13] add -json to tf init --- internal/command/init.go | 82 ++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/internal/command/init.go b/internal/command/init.go index 8900d6a3df48..ee1a35e8ab44 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -5,6 +5,7 @@ package command import ( "context" + "errors" "fmt" "log" "reflect" @@ -24,6 +25,7 @@ import ( 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" @@ -42,7 +44,7 @@ type InitCommand struct { func (c *InitCommand) Run(args []string) int { var flagFromModule, flagLockfile, testsDirectory string - var flagBackend, flagCloud, flagGet, flagUpgrade bool + var flagBackend, flagCloud, flagGet, flagUpgrade, flagJson bool var flagPluginPath FlagStringSlice flagConfigExtra := newRawFlags("-backend-config") @@ -63,6 +65,8 @@ func (c *InitCommand) Run(args []string) int { 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 @@ -92,6 +96,15 @@ 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 { @@ -102,12 +115,14 @@ func (c *InitCommand) Run(args []string) int { args = cmdFlags.Args() path, err := ModulePath(args) if err != nil { - c.Ui.Error(err.Error()) + diags = diags.Append(err) + view.Diagnostics(diags) return 1 } if err := c.storePluginPath(c.pluginPath); err != nil { - c.Ui.Error(fmt.Sprintf("Error saving -plugin-path values: %s", err)) + diags = diags.Append(fmt.Errorf("Error saving -plugin-path values: %s", err)) + view.Diagnostics(diags) return 1 } @@ -124,11 +139,13 @@ func (c *InitCommand) Run(args []string) int { empty, err := configs.IsEmptyDir(path) if err != nil { - c.Ui.Error(fmt.Sprintf("Error validating destination directory: %s", err)) + diags = diags.Append(fmt.Errorf("Error validating destination directory: %s", err)) + view.Diagnostics(diags) return 1 } if !empty { - c.Ui.Error(strings.TrimSpace(errInitCopyNotEmpty)) + diags = diags.Append(errors.New(strings.TrimSpace(errInitCopyNotEmpty))) + view.Diagnostics(diags) return 1 } @@ -149,7 +166,7 @@ func (c *InitCommand) Run(args []string) int { initDirFromModuleAbort, initDirFromModuleDiags := c.initDirFromModule(ctx, path, src, hooks) diags = diags.Append(initDirFromModuleDiags) if initDirFromModuleAbort || initDirFromModuleDiags.HasErrors() { - c.showDiagnostics(diags) + view.Diagnostics(diags) span.SetStatus(codes.Error, "module installation failed") span.End() return 1 @@ -164,7 +181,7 @@ func (c *InitCommand) Run(args []string) int { empty, err := configs.IsEmptyDir(path) if err != nil { diags = diags.Append(fmt.Errorf("Error checking configuration: %s", err)) - c.showDiagnostics(diags) + view.Diagnostics(diags) return 1 } if empty { @@ -182,7 +199,7 @@ func (c *InitCommand) Run(args []string) int { if rootModEarly == nil { c.Ui.Error(c.Colorize().Color(strings.TrimSpace(errInitConfigError))) diags = diags.Append(earlyConfDiags) - c.showDiagnostics(diags) + view.Diagnostics(diags) return 1 } @@ -196,9 +213,9 @@ func (c *InitCommand) Run(args []string) int { switch { case flagCloud && rootModEarly.CloudConfig != nil: - back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, flagConfigExtra) + back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, flagConfigExtra, viewType) case flagBackend: - back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, flagConfigExtra) + back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, flagConfigExtra, viewType) default: // load the previously-stored backend config back, backDiags = c.Meta.backendFromState(ctx) @@ -216,17 +233,20 @@ func (c *InitCommand) Run(args []string) int { c.ignoreRemoteVersionConflict(back) workspace, err := c.Workspace() if err != nil { - c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err)) + diags = diags.Append(fmt.Errorf("Error selecting workspace: %s", err)) + view.Diagnostics(diags) return 1 } sMgr, err := back.StateMgr(workspace) if err != nil { - c.Ui.Error(fmt.Sprintf("Error loading state: %s", err)) + diags = diags.Append(fmt.Errorf("Error loading state: %s", err)) + view.Diagnostics(diags) return 1 } if err := sMgr.RefreshState(); err != nil { - c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err)) + diags = diags.Append(fmt.Errorf("Error refreshing state: %s", err)) + view.Diagnostics(diags) return 1 } @@ -237,7 +257,7 @@ func (c *InitCommand) Run(args []string) int { modsOutput, modsAbort, modsDiags := c.getModules(ctx, path, testsDirectory, rootModEarly, flagUpgrade) diags = diags.Append(modsDiags) if modsAbort || modsDiags.HasErrors() { - c.showDiagnostics(diags) + view.Diagnostics(diags) return 1 } if modsOutput { @@ -258,7 +278,7 @@ func (c *InitCommand) Run(args []string) int { // potentially-confusing downstream errors. versionDiags := terraform.CheckCoreVersionRequirements(config) if versionDiags.HasErrors() { - c.showDiagnostics(versionDiags) + view.Diagnostics(versionDiags) return 1 } @@ -271,7 +291,7 @@ func (c *InitCommand) Run(args []string) int { diags = diags.Append(backDiags) if earlyConfDiags.HasErrors() { c.Ui.Error(strings.TrimSpace(errInitConfigError)) - c.showDiagnostics(diags) + view.Diagnostics(diags) return 1 } @@ -279,7 +299,7 @@ func (c *InitCommand) Run(args []string) int { // show the errInitConfigError preamble as we didn't detect problems with // the early configuration. if backDiags.HasErrors() { - c.showDiagnostics(diags) + view.Diagnostics(diags) return 1 } @@ -288,7 +308,7 @@ func (c *InitCommand) Run(args []string) int { diags = diags.Append(confDiags) if confDiags.HasErrors() { c.Ui.Error(strings.TrimSpace(errInitConfigError)) - c.showDiagnostics(diags) + view.Diagnostics(diags) return 1 } @@ -296,17 +316,17 @@ func (c *InitCommand) Run(args []string) int { if c.RunningInAutomation { if err := cb.AssertImportCompatible(config); err != nil { diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Compatibility error", err.Error())) - c.showDiagnostics(diags) + 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, flagUpgrade, flagPluginPath, flagLockfile) + providersOutput, providersAbort, providerDiags := c.getProviders(ctx, config, state, flagUpgrade, flagPluginPath, flagLockfile, view) diags = diags.Append(providerDiags) if providersAbort || providerDiags.HasErrors() { - c.showDiagnostics(diags) + view.Diagnostics(diags) return 1 } if providersOutput { @@ -322,7 +342,7 @@ func (c *InitCommand) Run(args []string) int { // 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. - c.showDiagnostics(diags) + view.Diagnostics(diags) _, cloud := back.(*cloud.Cloud) output := outputInitSuccess if cloud { @@ -398,7 +418,7 @@ 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) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { +func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extraConfig rawFlags, viewType arguments.ViewType) (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() @@ -417,8 +437,9 @@ func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extra backendConfig := root.CloudConfig.ToBackendConfig() opts := &BackendOpts{ - Config: &backendConfig, - Init: true, + Config: &backendConfig, + Init: true, + ViewType: viewType, } back, backDiags := c.Backend(opts) @@ -426,7 +447,7 @@ 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) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { +func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, extraConfig rawFlags, viewType arguments.ViewType) (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() @@ -502,6 +523,7 @@ the backend configuration is present and valid. Config: backendConfig, ConfigOverride: backendConfigOverride, Init: true, + ViewType: viewType, } back, backDiags := c.Backend(opts) @@ -511,7 +533,7 @@ the backend configuration is present and valid. // Load the complete module tree, and fetch any missing providers. // This method outputs its own Ui. -func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, state *states.State, upgrade bool, pluginDirs []string, flagLockfile string) (output, abort bool, diags tfdiags.Diagnostics) { +func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, state *states.State, upgrade bool, pluginDirs []string, flagLockfile string, view views.Init) (output, abort bool, diags tfdiags.Diagnostics) { ctx, span := tracer.Start(ctx, "install providers") defer span.End() @@ -881,7 +903,7 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, } newLocks, err := inst.EnsureProviderVersions(ctx, previousLocks, reqs, mode) if ctx.Err() == context.Canceled { - c.showDiagnostics(diags) + view.Diagnostics(diags) c.Ui.Error("Provider installation was canceled by an interrupt signal.") return true, true, diags } @@ -1089,6 +1111,7 @@ func (c *InitCommand) AutocompleteFlags() complete.Flags { "-lock": completePredictBoolean, "-lock-timeout": complete.PredictAnything, "-no-color": complete.PredictNothing, + "-json": complete.PredictNothing, "-plugin-dir": complete.PredictDirs(""), "-reconfigure": complete.PredictNothing, "-migrate-state": complete.PredictNothing, @@ -1151,6 +1174,9 @@ Options: -no-color If specified, output won't contain any color. + -json If specified, machine readable output will be + printed in JSON format. + -plugin-dir Directory containing plugin binaries. This overrides all default search paths for plugins, and prevents the automatic installation of plugins. This flag can be used From 26f01d699ac278507eb7c75a724d87eb9674c66f Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Tue, 26 Mar 2024 04:02:09 -0400 Subject: [PATCH 04/13] add test for -json support --- internal/command/init_test.go | 233 +++++++++++------- .../output.jsonlog | 2 + 2 files changed, 147 insertions(+), 88 deletions(-) create mode 100644 internal/command/testdata/init-with-tests-with-provider/output.jsonlog diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 2422b319682f..789af8dc2e11 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -15,10 +15,9 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/google/go-cmp/cmp" "github.com/hashicorp/cli" + version "github.com/hashicorp/go-version" "github.com/zclconf/go-cty/cty" - "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" @@ -184,6 +183,34 @@ func TestInit_get(t *testing.T) { } } +func TestInit_json(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("init-get"), td) + defer testChdir(t, td)() + + ui := new(cli.MockUi) + view, _ := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + args := []string{"-json"} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + // Check output + output := ui.OutputWriter.String() + if !strings.Contains(output, "foo in foo") { + t.Fatalf("doesn't look like we installed module 'foo': %s", output) + } +} + func TestInit_getUpgradeModules(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() @@ -315,7 +342,7 @@ func TestInit_backendConfigFile(t *testing.T) { t.Run("good-config-file", func(t *testing.T) { ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -325,7 +352,7 @@ func TestInit_backendConfigFile(t *testing.T) { } args := []string{"-backend-config", "input.config"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } // Read our saved backend config and verify we have our settings @@ -338,7 +365,7 @@ func TestInit_backendConfigFile(t *testing.T) { // the backend config file must not be a full terraform block t.Run("full-backend-config-file", func(t *testing.T) { ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -350,7 +377,7 @@ func TestInit_backendConfigFile(t *testing.T) { if code := c.Run(args); code != 1 { t.Fatalf("expected error, got success\n") } - if !strings.Contains(ui.ErrorWriter.String(), "Unsupported block type") { + if !strings.Contains(done(t).All(), "Unsupported block type") { t.Fatalf("wrong error: %s", ui.ErrorWriter) } }) @@ -358,7 +385,7 @@ func TestInit_backendConfigFile(t *testing.T) { // the backend config file must match the schema for the backend t.Run("invalid-config-file", func(t *testing.T) { ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -370,7 +397,7 @@ func TestInit_backendConfigFile(t *testing.T) { if code := c.Run(args); code != 1 { t.Fatalf("expected error, got success\n") } - if !strings.Contains(ui.ErrorWriter.String(), "Unsupported argument") { + if !strings.Contains(done(t).All(), "Unsupported argument") { t.Fatalf("wrong error: %s", ui.ErrorWriter) } }) @@ -378,7 +405,7 @@ func TestInit_backendConfigFile(t *testing.T) { // missing file is an error t.Run("missing-config-file", func(t *testing.T) { ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -390,7 +417,7 @@ func TestInit_backendConfigFile(t *testing.T) { if code := c.Run(args); code != 1 { t.Fatalf("expected error, got success\n") } - if !strings.Contains(ui.ErrorWriter.String(), "Failed to read file") { + if !strings.Contains(done(t).All(), "Failed to read file") { t.Fatalf("wrong error: %s", ui.ErrorWriter) } }) @@ -398,7 +425,7 @@ func TestInit_backendConfigFile(t *testing.T) { // blank filename clears the backend config t.Run("blank-config-file", func(t *testing.T) { ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -408,7 +435,7 @@ func TestInit_backendConfigFile(t *testing.T) { } args := []string{"-backend-config=", "-migrate-state"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } // Read our saved backend config and verify the backend config is empty @@ -450,7 +477,7 @@ func TestInit_backendConfigFilePowershellConfusion(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -468,12 +495,13 @@ func TestInit_backendConfigFilePowershellConfusion(t *testing.T) { // result in an early exit with a diagnostic that the provided // configuration file is not a diretory. args := []string{"-backend-config=", "./input.config"} - if code := c.Run(args); code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + code := c.Run(args) + output := done(t) + if code != 1 { + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, output.Stderr(), output.Stdout()) } - output := ui.ErrorWriter.String() - if got, want := output, `Too many command line arguments`; !strings.Contains(got, want) { + if got, want := output.Stderr(), `Too many command line arguments`; !strings.Contains(got, want) { t.Fatalf("wrong output\ngot:\n%s\n\nwant: message containing %q", got, want) } } @@ -613,10 +641,13 @@ func TestInit_backendConfigFileChangeWithExistingState(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) + view, _ := testView(t) + c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -786,7 +817,7 @@ func TestInit_backendCli_no_config_block(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -800,7 +831,7 @@ func TestInit_backendCli_no_config_block(t *testing.T) { t.Fatalf("got exit status %d; want 0\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) } - errMsg := ui.ErrorWriter.String() + errMsg := done(t).All() if !strings.Contains(errMsg, "Warning: Missing backend configuration") { t.Fatal("expected missing backend block warning, got", errMsg) } @@ -978,7 +1009,7 @@ func TestInit_backendCloudInvalidOptions(t *testing.T) { // configuration is only about which workspaces we'll be working // with. ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -990,7 +1021,7 @@ func TestInit_backendCloudInvalidOptions(t *testing.T) { t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).All() wantStderr := ` Error: Invalid command-line option @@ -999,7 +1030,6 @@ is not applicable to Terraform Cloud-based configurations. To change the set of workspaces associated with this configuration, edit the Cloud configuration block in the root module. - ` if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { t.Errorf("wrong error output\n%s", diff) @@ -1017,7 +1047,7 @@ Cloud configuration block in the root module. // -reconfigure doesn't really make sense in that context, particularly // with its design bug with the handling of the implicit local backend. ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -1029,7 +1059,7 @@ Cloud configuration block in the root module. t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).All() wantStderr := ` Error: Invalid command-line option @@ -1038,7 +1068,6 @@ only, and is not needed when changing Terraform Cloud settings. When using Terraform Cloud, initialization automatically activates any new Cloud configuration settings. - ` if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { t.Errorf("wrong error output\n%s", diff) @@ -1056,7 +1085,7 @@ Cloud configuration settings. } ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -1068,13 +1097,12 @@ Cloud configuration settings. t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).All() wantStderr := ` Error: Invalid command-line option The -reconfigure option is unsupported when migrating to Terraform Cloud, because activating Terraform Cloud involves some additional steps. - ` if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { t.Errorf("wrong error output\n%s", diff) @@ -1087,7 +1115,7 @@ because activating Terraform Cloud involves some additional steps. // and changing configuration while staying in cloud mode never migrates // state, so this special option isn't relevant. ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -1099,7 +1127,7 @@ because activating Terraform Cloud involves some additional steps. t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).All() wantStderr := ` Error: Invalid command-line option @@ -1108,7 +1136,6 @@ is not applicable when using Terraform Cloud. State storage is handled automatically by Terraform Cloud and so the state storage location is not configurable. - ` if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { t.Errorf("wrong error output\n%s", diff) @@ -1126,7 +1153,7 @@ storage location is not configurable. } ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -1138,7 +1165,7 @@ storage location is not configurable. t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).All() wantStderr := ` Error: Invalid command-line option @@ -1147,7 +1174,6 @@ is not applicable when using Terraform Cloud. Terraform Cloud migration has additional steps, configured by interactive prompts. - ` if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { t.Errorf("wrong error output\n%s", diff) @@ -1160,7 +1186,7 @@ prompts. // and changing configuration while staying in cloud mode never migrates // state, so this special option isn't relevant. ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -1172,7 +1198,7 @@ prompts. t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).All() wantStderr := ` Error: Invalid command-line option @@ -1181,7 +1207,6 @@ not applicable when using Terraform Cloud. State storage is handled automatically by Terraform Cloud and so the state storage location is not configurable. - ` if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { t.Errorf("wrong error output\n%s", diff) @@ -1199,7 +1224,7 @@ storage location is not configurable. } ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -1211,7 +1236,7 @@ storage location is not configurable. t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).All() wantStderr := ` Error: Invalid command-line option @@ -1220,7 +1245,6 @@ not applicable when using Terraform Cloud. Terraform Cloud migration has additional steps, configured by interactive prompts. - ` if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { t.Errorf("wrong error output\n%s", diff) @@ -1236,7 +1260,7 @@ func TestInit_inputFalse(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -1286,7 +1310,7 @@ func TestInit_inputFalse(t *testing.T) { t.Fatal("init should have failed", ui.OutputWriter) } - errMsg := ui.ErrorWriter.String() + errMsg := done(t).All() if !strings.Contains(errMsg, "interactive input is disabled") { t.Fatal("expected input disabled error, got", errMsg) } @@ -1394,7 +1418,7 @@ func TestInit_getProvider(t *testing.T) { } ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m.Ui = ui m.View = view c := &InitCommand{ @@ -1405,7 +1429,7 @@ func TestInit_getProvider(t *testing.T) { t.Fatal("expected error, got:", ui.OutputWriter) } - errMsg := ui.ErrorWriter.String() + errMsg := done(t).All() if !strings.Contains(errMsg, "Unsupported state file format") { t.Fatal("unexpected error:", errMsg) } @@ -1470,7 +1494,7 @@ func TestInit_getProviderLegacyFromState(t *testing.T) { overrides := metaOverridesForProvider(testProvider()) ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) providerSource, close := newMockProviderSource(t, map[string][]string{ "acme/alpha": {"1.2.3"}, }) @@ -1495,7 +1519,7 @@ func TestInit_getProviderLegacyFromState(t *testing.T) { "Invalid legacy provider address", "You must complete the Terraform 0.13 upgrade process", } - got := ui.ErrorWriter.String() + got := done(t).All() for _, want := range wants { if !strings.Contains(got, want) { t.Fatalf("expected output to contain %q, got:\n\n%s", want, got) @@ -1511,7 +1535,7 @@ func TestInit_getProviderInvalidPackage(t *testing.T) { overrides := metaOverridesForProvider(testProvider()) ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) // create a provider source which allows installing an invalid package addr := addrs.MustParseProviderSourceString("invalid/package") @@ -1557,7 +1581,7 @@ func TestInit_getProviderInvalidPackage(t *testing.T) { "Failed to install provider", "could not find executable file starting with terraform-provider-package", } - got := ui.ErrorWriter.String() + got := done(t).All() for _, wantError := range wantErrors { if !strings.Contains(got, wantError) { t.Fatalf("missing error:\nwant: %q\ngot:\n%s", wantError, got) @@ -1588,7 +1612,7 @@ func TestInit_getProviderDetectedLegacy(t *testing.T) { } ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m := Meta{ Ui: ui, View: view, @@ -1618,7 +1642,7 @@ func TestInit_getProviderDetectedLegacy(t *testing.T) { } // error output is the main focus of this test - errOutput := ui.ErrorWriter.String() + errOutput := done(t).All() errors := []string{ "Failed to query available provider packages", "Could not retrieve the list of available versions", @@ -1646,7 +1670,7 @@ func TestInit_providerSource(t *testing.T) { defer close() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -1735,7 +1759,7 @@ func TestInit_providerSource(t *testing.T) { if got, want := ui.OutputWriter.String(), "Installed hashicorp/test v1.2.3 (verified checksum)"; !strings.Contains(got, want) { t.Fatalf("unexpected output: %s\nexpected to include %q", got, want) } - if got, want := ui.ErrorWriter.String(), "\n - hashicorp/source\n - hashicorp/test\n - hashicorp/test-beta"; !strings.Contains(got, want) { + if got, want := done(t).All(), "\n - hashicorp/source\n - hashicorp/test\n - hashicorp/test-beta"; !strings.Contains(got, want) { t.Fatalf("wrong error message\nshould contain: %s\ngot:\n%s", want, got) } } @@ -1965,7 +1989,7 @@ func TestInit_getProviderMissing(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -1982,7 +2006,7 @@ func TestInit_getProviderMissing(t *testing.T) { t.Fatalf("expected error, got output: \n%s", ui.OutputWriter.String()) } - if !strings.Contains(ui.ErrorWriter.String(), "no available releases match") { + if !strings.Contains(done(t).All(), "no available releases match") { t.Fatalf("unexpected error output: %s", ui.ErrorWriter) } } @@ -1994,7 +2018,7 @@ func TestInit_checkRequiredVersion(t *testing.T) { defer testChdir(t, td)() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -2005,9 +2029,9 @@ func TestInit_checkRequiredVersion(t *testing.T) { args := []string{} if code := c.Run(args); code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, done(t).All(), ui.OutputWriter.String()) } - errStr := ui.ErrorWriter.String() + errStr := done(t).All() if !strings.Contains(errStr, `required_version = "~> 0.9.0"`) { t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr) } @@ -2025,7 +2049,7 @@ func TestInit_checkRequiredVersionFirst(t *testing.T) { defer testChdir(t, td)() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -2036,9 +2060,9 @@ func TestInit_checkRequiredVersionFirst(t *testing.T) { args := []string{} if code := c.Run(args); code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, done(t).All(), ui.OutputWriter.String()) } - errStr := ui.ErrorWriter.String() + errStr := done(t).All() if !strings.Contains(errStr, `Unsupported Terraform Core version`) { t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr) } @@ -2049,7 +2073,7 @@ func TestInit_checkRequiredVersionFirst(t *testing.T) { defer testChdir(t, td)() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -2060,9 +2084,9 @@ func TestInit_checkRequiredVersionFirst(t *testing.T) { args := []string{} if code := c.Run(args); code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, done(t).All(), ui.OutputWriter.String()) } - errStr := ui.ErrorWriter.String() + errStr := done(t).All() if !strings.Contains(errStr, `Unsupported Terraform Core version`) { t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr) } @@ -2083,7 +2107,7 @@ func TestInit_providerLockFile(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -2128,7 +2152,7 @@ provider "registry.terraform.io/hashicorp/test" { // succeeds, to ensure that we don't try to rewrite an unchanged lock file os.Chmod(".", 0555) if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } } @@ -2268,9 +2292,11 @@ provider "registry.terraform.io/hashicorp/test" { defer close() ui := new(cli.MockUi) + view, _ := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, ProviderSource: providerSource, } @@ -2485,7 +2511,7 @@ func TestInit_pluginDirProvidersDoesNotGet(t *testing.T) { defer close() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -2531,7 +2557,7 @@ func TestInit_pluginDirProvidersDoesNotGet(t *testing.T) { // The error output should mention the "between" provider but should not // mention either the "exact" or "greater-than" provider, because the // latter two are available via the -plugin-dir directories. - errStr := ui.ErrorWriter.String() + errStr := done(t).All() if subStr := "hashicorp/between"; !strings.Contains(errStr, subStr) { t.Errorf("error output should mention the 'between' provider\nwant substr: %s\ngot:\n%s", subStr, errStr) } @@ -2596,7 +2622,7 @@ func TestInit_invalidBuiltInProviders(t *testing.T) { defer close() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -2612,7 +2638,7 @@ func TestInit_invalidBuiltInProviders(t *testing.T) { t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) } - errStr := ui.ErrorWriter.String() + errStr := done(t).All() if subStr := "Cannot use terraform.io/builtin/terraform: built-in"; !strings.Contains(errStr, subStr) { t.Errorf("error output should mention the terraform provider\nwant substr: %s\ngot:\n%s", subStr, errStr) } @@ -2627,7 +2653,7 @@ func TestInit_invalidSyntaxNoBackend(t *testing.T) { defer testChdir(t, td)() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ Ui: ui, View: view, @@ -2641,9 +2667,9 @@ func TestInit_invalidSyntaxNoBackend(t *testing.T) { t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) } - errStr := ui.ErrorWriter.String() - if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(errStr, subStr) { - t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, errStr) + errStr := done(t).All() + if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(ui.ErrorWriter.String(), subStr) { + t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, ui.ErrorWriter.String()) } if subStr := "Error: Unsupported block type"; !strings.Contains(errStr, subStr) { t.Errorf("Error output should mention the syntax problem\nwant substr: %s\ngot:\n%s", subStr, errStr) @@ -2656,7 +2682,7 @@ func TestInit_invalidSyntaxWithBackend(t *testing.T) { defer testChdir(t, td)() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ Ui: ui, View: view, @@ -2670,9 +2696,9 @@ func TestInit_invalidSyntaxWithBackend(t *testing.T) { t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) } - errStr := ui.ErrorWriter.String() - if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(errStr, subStr) { - t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, errStr) + errStr := done(t).All() + if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(ui.ErrorWriter.String(), subStr) { + t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, ui.ErrorWriter.String()) } if subStr := "Error: Unsupported block type"; !strings.Contains(errStr, subStr) { t.Errorf("Error output should mention the syntax problem\nwant substr: %s\ngot:\n%s", subStr, errStr) @@ -2685,7 +2711,7 @@ func TestInit_invalidSyntaxInvalidBackend(t *testing.T) { defer testChdir(t, td)() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ Ui: ui, View: view, @@ -2699,9 +2725,9 @@ func TestInit_invalidSyntaxInvalidBackend(t *testing.T) { t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) } - errStr := ui.ErrorWriter.String() - if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(errStr, subStr) { - t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, errStr) + errStr := done(t).All() + if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(ui.ErrorWriter.String(), subStr) { + t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, ui.ErrorWriter.String()) } if subStr := "Error: Unsupported block type"; !strings.Contains(errStr, subStr) { t.Errorf("Error output should mention syntax errors\nwant substr: %s\ngot:\n%s", subStr, errStr) @@ -2717,7 +2743,7 @@ func TestInit_invalidSyntaxBackendAttribute(t *testing.T) { defer testChdir(t, td)() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ Ui: ui, View: view, @@ -2731,9 +2757,9 @@ func TestInit_invalidSyntaxBackendAttribute(t *testing.T) { t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) } - errStr := ui.ErrorWriter.String() - if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(errStr, subStr) { - t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, errStr) + errStr := done(t).All() + if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(ui.ErrorWriter.String(), subStr) { + t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, ui.ErrorWriter.String()) } if subStr := "Error: Invalid character"; !strings.Contains(errStr, subStr) { t.Errorf("Error output should mention the invalid character\nwant substr: %s\ngot:\n%s", subStr, errStr) @@ -2787,7 +2813,7 @@ func TestInit_testsWithProvider(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(provider), @@ -2802,20 +2828,51 @@ func TestInit_testsWithProvider(t *testing.T) { t.Fatalf("expected failure but got: \n%s", ui.OutputWriter.String()) } - got := ui.ErrorWriter.String() + got := done(t).All() want := ` Error: Failed to query available provider packages Could not retrieve the list of available versions for provider hashicorp/test: no available releases match the given constraints 1.0.1, 1.0.2 - ` if diff := cmp.Diff(got, want); len(diff) > 0 { t.Fatalf("wrong error message: \ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff) } } +func TestInit_jsonTestsWithProvider(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("init-with-tests-with-provider"), td) + defer testChdir(t, td)() + + provider := applyFixtureProvider() // We just want the types from this provider. + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.0.0"}, + }) + defer close() + + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider), + Ui: ui, + View: view, + ProviderSource: providerSource, + }, + } + + args := []string{"-json"} + if code := c.Run(args); code == 0 { + t.Fatalf("expected failure but got: \n%s", ui.OutputWriter.String()) + } + + checkGoldenReference(t, done(t), "init-with-tests-with-provider") +} + func TestInit_testsWithModule(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() diff --git a/internal/command/testdata/init-with-tests-with-provider/output.jsonlog b/internal/command/testdata/init-with-tests-with-provider/output.jsonlog new file mode 100644 index 000000000000..aad930f3fedf --- /dev/null +++ b/internal/command/testdata/init-with-tests-with-provider/output.jsonlog @@ -0,0 +1,2 @@ +{"@level":"info","@message":"Terraform 1.9.0-dev","@module":"terraform.ui","terraform":"1.9.0-dev","type":"version","ui":"1.2"} +{"@level":"error","@message":"Error: Failed to query available provider packages","@module":"terraform.ui","diagnostic":{"severity":"error","summary":"Failed to query available provider packages","detail":"Could not retrieve the list of available versions for provider hashicorp/test: no available releases match the given constraints 1.0.1, 1.0.2"},"type":"diagnostic"} \ No newline at end of file From 370a471ecb4d940550d040e9df45d4bcfcd18408 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Thu, 28 Mar 2024 00:40:24 -0400 Subject: [PATCH 05/13] fix json -help display and failing tests --- internal/command/e2etest/init_test.go | 3 +- internal/command/init.go | 4 +- internal/command/providers_schema_test.go | 2 + internal/command/providers_test.go | 2 + internal/command/show_test.go | 8 +++ internal/command/test_test.go | 66 ++++++++++++++++++----- internal/command/validate_test.go | 14 +++++ 7 files changed, 83 insertions(+), 16 deletions(-) diff --git a/internal/command/e2etest/init_test.go b/internal/command/e2etest/init_test.go index e1520c8cd966..abe2fb111db8 100644 --- a/internal/command/e2etest/init_test.go +++ b/internal/command/e2etest/init_test.go @@ -374,14 +374,13 @@ func TestInitProviderNotFound(t *testing.T) { │ Could not retrieve the list of available versions for provider │ hashicorp/nonexist: provider registry registry.terraform.io does not have a │ provider named registry.terraform.io/hashicorp/nonexist -│ +│` + ` ` + ` │ All modules should specify their required_providers so that external │ consumers will get the correct providers when using a module. To see which │ modules are currently depending on hashicorp/nonexist, run the following │ command: │ terraform providers ╵ - ` if stripAnsi(stderr) != expectedErr { t.Errorf("wrong output:\n%s", cmp.Diff(stripAnsi(stderr), expectedErr)) diff --git a/internal/command/init.go b/internal/command/init.go index ee1a35e8ab44..4993c2d9828c 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -1174,8 +1174,8 @@ Options: -no-color If specified, output won't contain any color. - -json If specified, machine readable output will be - printed in JSON format. + -json If specified, machine readable output will be + printed in JSON format. -plugin-dir Directory containing plugin binaries. This overrides all default search paths for plugins, and prevents the diff --git a/internal/command/providers_schema_test.go b/internal/command/providers_schema_test.go index ce3ee2425cf3..bcbd48a65cee 100644 --- a/internal/command/providers_schema_test.go +++ b/internal/command/providers_schema_test.go @@ -59,9 +59,11 @@ func TestProvidersSchema_output(t *testing.T) { p := providersSchemaFixtureProvider() ui := new(cli.MockUi) + view, _ := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, ProviderSource: providerSource, } diff --git a/internal/command/providers_test.go b/internal/command/providers_test.go index 56f5c0fd7eb4..98c0c1fe190d 100644 --- a/internal/command/providers_test.go +++ b/internal/command/providers_test.go @@ -84,6 +84,7 @@ func TestProviders_modules(t *testing.T) { // first run init with mock provider sources to install the module initUi := new(cli.MockUi) + view, _ := testView(t) providerSource, close := newMockProviderSource(t, map[string][]string{ "foo": {"1.0.0"}, "bar": {"2.0.0"}, @@ -93,6 +94,7 @@ func TestProviders_modules(t *testing.T) { m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: initUi, + View: view, ProviderSource: providerSource, } ic := &InitCommand{ diff --git a/internal/command/show_test.go b/internal/command/show_test.go index cc549e860edf..a8c369066b84 100644 --- a/internal/command/show_test.go +++ b/internal/command/show_test.go @@ -558,10 +558,12 @@ func TestShow_json_output(t *testing.T) { // init ui := new(cli.MockUi) + view, _ := testView(t) ic := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, ProviderSource: providerSource, }, } @@ -666,10 +668,12 @@ func TestShow_json_output_sensitive(t *testing.T) { // init ui := new(cli.MockUi) + view, _ := testView(t) ic := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, ProviderSource: providerSource, }, } @@ -759,10 +763,12 @@ func TestShow_json_output_conditions_refresh_only(t *testing.T) { // init ui := new(cli.MockUi) + view, _ := testView(t) ic := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, ProviderSource: providerSource, }, } @@ -868,10 +874,12 @@ func TestShow_json_output_state(t *testing.T) { // init ui := new(cli.MockUi) + view, _ := testView(t) ic := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, ProviderSource: providerSource, }, } diff --git a/internal/command/test_test.go b/internal/command/test_test.go index bf8a1f3e36d9..a1f4f4dca56f 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -290,8 +290,8 @@ func TestTest_Runs(t *testing.T) { if tc.initCode > 0 { // Then we don't expect the init step to succeed. So we'll check // the init output for our expected error messages and outputs. - - stdout, stderr := ui.ErrorWriter.String(), ui.ErrorWriter.String() + output := done(t).All() + stdout, stderr := output, output if !strings.Contains(stdout, tc.expectedOut) { t.Errorf("output didn't contain expected string:\n\n%s", stdout) @@ -872,8 +872,8 @@ can remove the provider configuration again. actualOut, expectedOut := output.Stdout(), tc.expectedOut actualErr, expectedErr := output.Stderr(), tc.expectedErr - if diff := cmp.Diff(actualOut, expectedOut); len(diff) > 0 { - t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedOut, actualOut, diff) + if !strings.Contains(actualOut, expectedOut) { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s", expectedOut, actualOut) } if diff := cmp.Diff(actualErr, expectedErr); len(diff) > 0 { @@ -1063,8 +1063,8 @@ Success! 5 passed, 0 failed. actual := output.All() - if diff := cmp.Diff(actual, expected); len(diff) > 0 { - t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + if !strings.Contains(actual, expected) { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s", expected, actual) } if provider.ResourceCount() > 0 { @@ -1124,10 +1124,10 @@ main.tftest.hcl... pass Success! 2 passed, 0 failed. ` - actual := output.All() + actual := output.Stdout() - if diff := cmp.Diff(actual, expected); len(diff) > 0 { - t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + if !strings.Contains(actual, expected) { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s", expected, actual) } if provider.ResourceCount() > 0 { @@ -1915,7 +1915,21 @@ func TestTest_InvalidOverrides(t *testing.T) { t.Errorf("expected status code 0 but got %d", code) } - expected := `main.tftest.hcl... in progress + expected := ` +Warning: Incomplete lock file information for providers + +Due to your customized provider installation methods, Terraform was forced to +calculate lock file checksums locally for the following providers: + - hashicorp/test + +The current .terraform.lock.hcl file only includes checksums for linux_amd64, +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) +main.tftest.hcl... in progress run "setup"... pass Warning: Invalid override target @@ -2009,7 +2023,21 @@ func TestTest_RunBlocksInProviders(t *testing.T) { t.Errorf("expected status code 0 but got %d", code) } - expected := `main.tftest.hcl... in progress + expected := ` +Warning: Incomplete lock file information for providers + +Due to your customized provider installation methods, Terraform was forced to +calculate lock file checksums locally for the following providers: + - hashicorp/test + +The current .terraform.lock.hcl file only includes checksums for linux_amd64, +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) +main.tftest.hcl... in progress run "setup"... pass run "main"... pass main.tftest.hcl... tearing down @@ -2070,7 +2098,21 @@ func TestTest_RunBlocksInProviders_BadReferences(t *testing.T) { t.Errorf("expected status code 1 but got %d", code) } - expectedOut := `missing_run_block.tftest.hcl... in progress + expectedOut := ` +Warning: Incomplete lock file information for providers + +Due to your customized provider installation methods, Terraform was forced to +calculate lock file checksums locally for the following providers: + - hashicorp/test + +The current .terraform.lock.hcl file only includes checksums for linux_amd64, +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) +missing_run_block.tftest.hcl... in progress run "main"... fail missing_run_block.tftest.hcl... tearing down missing_run_block.tftest.hcl... fail diff --git a/internal/command/validate_test.go b/internal/command/validate_test.go index cc960fb87b66..d7a7e94ff068 100644 --- a/internal/command/validate_test.go +++ b/internal/command/validate_test.go @@ -362,6 +362,20 @@ func TestValidateWithInvalidOverrides(t *testing.T) { actual := output.All() expected := ` +Warning: Incomplete lock file information for providers + +Due to your customized provider installation methods, Terraform was forced to +calculate lock file checksums locally for the following providers: + - hashicorp/test + +The current .terraform.lock.hcl file only includes checksums for linux_amd64, +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) + Warning: Invalid override target on main.tftest.hcl line 4, in mock_provider "test": From ea8d0869d8ee55db1743979941d941523fb770b8 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Fri, 5 Apr 2024 01:30:55 -0400 Subject: [PATCH 06/13] 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 000000000000..fc4a702769d9 --- /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 a795ac63805b..9971cfbd32e8 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 4993c2d9828c..a869503ce803 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 80967a282000..a53a80a28201 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. +` From ba8386b8a2b0af7d66e86ff92fb5b993c2ba92d2 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Sun, 7 Apr 2024 12:18:26 -0400 Subject: [PATCH 07/13] json does not support interactive prompt so dont allow setting of both -migrate-state and -json options --- internal/command/init.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/internal/command/init.go b/internal/command/init.go index a869503ce803..f382c1c3e2a4 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -77,6 +77,16 @@ func (c *InitCommand) Run(args []string) int { c.migrateState = initArgs.MigrateState c.Meta.ignoreRemoteVersion = initArgs.IgnoreRemoteVersion + if initArgs.MigrateState && initArgs.Json { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "The -migrate-state and -json options are mutually-exclusive", + "Terraform cannot ask for interactive approval when -json is set. To use the -migrate-state option, disable the -json option.", + )) + view.Diagnostics(diags) + return 1 + } + if c.migrateState && c.reconfigure { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, From 0ea25e3b403a91656ababc7df8883e48431d4245 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Sun, 7 Apr 2024 19:31:23 -0400 Subject: [PATCH 08/13] fixing tests internal/command/init, view and argumento --- internal/command/arguments/init_test.go | 136 ++++++ internal/command/init.go | 2 +- internal/command/init_test.go | 437 ++++++++++-------- internal/command/providers_schema_test.go | 7 +- internal/command/test_test.go | 84 +++- .../command/testdata/init-get/output.jsonlog | 8 + .../init-migrate-state-with-json/hello.tf | 0 .../output.jsonlog | 2 + .../output.jsonlog | 2 - internal/command/validate_test.go | 24 +- internal/command/views/init.go | 45 +- internal/command/views/init_test.go | 237 +++++++++- internal/command/views/json_view_test.go | 2 +- 13 files changed, 723 insertions(+), 263 deletions(-) create mode 100644 internal/command/arguments/init_test.go create mode 100644 internal/command/testdata/init-get/output.jsonlog create mode 100644 internal/command/testdata/init-migrate-state-with-json/hello.tf create mode 100644 internal/command/testdata/init-migrate-state-with-json/output.jsonlog delete mode 100644 internal/command/testdata/init-with-tests-with-provider/output.jsonlog diff --git a/internal/command/arguments/init_test.go b/internal/command/arguments/init_test.go new file mode 100644 index 000000000000..6dacbf6eff2e --- /dev/null +++ b/internal/command/arguments/init_test.go @@ -0,0 +1,136 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "flag" + "io" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +func TestParseInit_basicValid(t *testing.T) { + testCases := map[string]struct { + args []string + want *Init + }{ + "with default options": { + nil, + &Init{ + FromModule: "", + Lockfile: "", + TestsDirectory: "tests", + ViewType: ViewHuman, + Backend: true, + Cloud: true, + Get: true, + ForceInitCopy: false, + StateLock: true, + StateLockTimeout: 0, + Reconfigure: false, + MigrateState: false, + Upgrade: false, + Json: false, + IgnoreRemoteVersion: false, + }, + }, + "setting multiple options": { + []string{"-backend=false", "-force-copy=true", + "-from-module=./main-dir", "-json", "-get=false", + "-lock=false", "-lock-timeout=10s", "-reconfigure=true", + "-upgrade=true", "-lockfile=readonly", + "-ignore-remote-version=true", "-test-directory=./test-dir"}, + &Init{ + FromModule: "./main-dir", + Lockfile: "readonly", + TestsDirectory: "./test-dir", + ViewType: ViewJSON, + Backend: false, + Cloud: false, + Get: false, + ForceInitCopy: true, + StateLock: false, + StateLockTimeout: time.Duration(10) * time.Second, + Reconfigure: true, + MigrateState: false, + Upgrade: true, + Json: true, + IgnoreRemoteVersion: true, + }, + }, + "with cloud option": { + []string{"-cloud=false"}, + &Init{ + FromModule: "", + Lockfile: "", + TestsDirectory: "tests", + ViewType: ViewHuman, + Backend: false, + Cloud: false, + Get: true, + ForceInitCopy: false, + StateLock: true, + StateLockTimeout: 0, + Reconfigure: false, + MigrateState: false, + Upgrade: false, + Json: false, + IgnoreRemoteVersion: false, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError) + cmdFlags.SetOutput(io.Discard) + + got, diags := ParseInit(tc.args, cmdFlags) + if len(diags) > 0 { + t.Fatalf("unexpected diags: %v", diags) + } + + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("unexpected result\n%s", diff) + } + }) + } +} + +func TestParseInit_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + wantErr string + }{ + "with unsupported options": { + args: []string{"-raw"}, + wantErr: "flag provided but not defined", + }, + "with both -backend and -cloud options set": { + args: []string{"-backend=false", "-cloud=false"}, + wantErr: "The -backend and -cloud options are aliases of one another and mutually-exclusive in their use", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError) + cmdFlags.SetOutput(io.Discard) + + got, diags := ParseInit(tc.args, cmdFlags) + if len(diags) == 0 { + t.Fatal("expected diags but got none") + } + if got, want := diags.Err().Error(), tc.wantErr; !strings.Contains(got, want) { + t.Fatalf("wrong diags\n got: %s\nwant: %s", got, want) + } + if got.ViewType != ViewHuman { + t.Fatalf("wrong view type, got %#v, want %#v", got.ViewType, ViewHuman) + } + }) + } +} diff --git a/internal/command/init.go b/internal/command/init.go index f382c1c3e2a4..83800452ecf2 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -148,7 +148,7 @@ func (c *InitCommand) Run(args []string) int { view.Output(views.CopyingConfigurationMessage, src) header = true - hooks := uiModuleInstallHooks{ // here check to verify if downloading prints text, update to handle view type + hooks := uiModuleInstallHooks{ Ui: c.Ui, ShowLocalPaths: false, // since they are in a weird location for init View: view, diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 789af8dc2e11..62ad7f284674 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -36,7 +36,7 @@ func TestInit_empty(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -47,7 +47,7 @@ func TestInit_empty(t *testing.T) { args := []string{} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } } @@ -58,7 +58,7 @@ func TestInit_multipleArgs(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -72,10 +72,40 @@ func TestInit_multipleArgs(t *testing.T) { "bad", } if code := c.Run(args); code != 1 { - t.Fatalf("bad: \n%s", ui.OutputWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } } +func TestInit_migrateStateAndJSON(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + os.MkdirAll(td, 0755) + defer testChdir(t, td)() + + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + args := []string{ + "-migrate-state=true", + "-json=true", + } + code := c.Run(args) + testOutput := done(t) + if code != 1 { + t.Fatalf("error, -migrate-state and -json should be exclusive: \n%s", testOutput.All()) + } + + // Check output + checkGoldenReference(t, testOutput, "init-migrate-state-with-json") +} + func TestInit_fromModule_cwdDest(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() @@ -83,7 +113,7 @@ func TestInit_fromModule_cwdDest(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -96,7 +126,7 @@ func TestInit_fromModule_cwdDest(t *testing.T) { "-from-module=" + testFixturePath("init"), } if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } if _, err := os.Stat(filepath.Join(td, "hello.tf")); err != nil { @@ -134,7 +164,7 @@ func TestInit_fromModule_dstInSrc(t *testing.T) { } ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -147,7 +177,7 @@ func TestInit_fromModule_dstInSrc(t *testing.T) { "-from-module=./..", } if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } if _, err := os.Stat(filepath.Join(dir, "foo", "issue518.tf")); err != nil { @@ -162,7 +192,7 @@ func TestInit_get(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -173,11 +203,11 @@ func TestInit_get(t *testing.T) { args := []string{} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } // Check output - output := ui.OutputWriter.String() + output := done(t).Stdout() if !strings.Contains(output, "foo in foo") { t.Fatalf("doesn't look like we installed module 'foo': %s", output) } @@ -190,7 +220,7 @@ func TestInit_json(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -201,14 +231,12 @@ func TestInit_json(t *testing.T) { args := []string{"-json"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } // Check output - output := ui.OutputWriter.String() - if !strings.Contains(output, "foo in foo") { - t.Fatalf("doesn't look like we installed module 'foo': %s", output) - } + output := done(t) + checkGoldenReference(t, output, "init-get") } func TestInit_getUpgradeModules(t *testing.T) { @@ -218,7 +246,7 @@ func TestInit_getUpgradeModules(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -231,14 +259,15 @@ func TestInit_getUpgradeModules(t *testing.T) { "-get=true", "-upgrade", } - if code := c.Run(args); code != 0 { - t.Fatalf("command did not complete successfully:\n%s", ui.ErrorWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("command did not complete successfully:\n%s", testOutput.Stderr()) } // Check output - output := ui.OutputWriter.String() - if !strings.Contains(output, "Upgrading modules...") { - t.Fatalf("doesn't look like get upgrade: %s", output) + if !strings.Contains(testOutput.Stdout(), "Upgrading modules...") { + t.Fatalf("doesn't look like get upgrade: %s", testOutput.Stdout()) } } @@ -249,7 +278,7 @@ func TestInit_backend(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -260,7 +289,7 @@ func TestInit_backend(t *testing.T) { args := []string{} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } if _, err := os.Stat(filepath.Join(DefaultDataDir, DefaultStateFilename)); err != nil { @@ -278,7 +307,7 @@ func TestInit_backendUnset(t *testing.T) { log.Printf("[TRACE] TestInit_backendUnset: beginning first init") ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -289,12 +318,14 @@ func TestInit_backendUnset(t *testing.T) { // Init args := []string{} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", testOutput.All()) } log.Printf("[TRACE] TestInit_backendUnset: first init complete") - t.Logf("First run output:\n%s", ui.OutputWriter.String()) - t.Logf("First run errors:\n%s", ui.ErrorWriter.String()) + t.Logf("First run output:\n%s", testOutput.Stdout()) + t.Logf("First run errors:\n%s", testOutput.Stderr()) if _, err := os.Stat(filepath.Join(DefaultDataDir, DefaultStateFilename)); err != nil { t.Fatalf("err: %s", err) @@ -310,7 +341,7 @@ func TestInit_backendUnset(t *testing.T) { } ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -320,12 +351,14 @@ func TestInit_backendUnset(t *testing.T) { } args := []string{"-force-copy"} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", testOutput.All()) } log.Printf("[TRACE] TestInit_backendUnset: second init complete") - t.Logf("Second run output:\n%s", ui.OutputWriter.String()) - t.Logf("Second run errors:\n%s", ui.ErrorWriter.String()) + t.Logf("Second run output:\n%s", testOutput.Stdout()) + t.Logf("Second run errors:\n%s", testOutput.Stderr()) s := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) if !s.Backend.Empty() { @@ -378,7 +411,7 @@ func TestInit_backendConfigFile(t *testing.T) { t.Fatalf("expected error, got success\n") } if !strings.Contains(done(t).All(), "Unsupported block type") { - t.Fatalf("wrong error: %s", ui.ErrorWriter) + t.Fatalf("wrong error: %s", done(t).Stderr()) } }) @@ -398,7 +431,7 @@ func TestInit_backendConfigFile(t *testing.T) { t.Fatalf("expected error, got success\n") } if !strings.Contains(done(t).All(), "Unsupported argument") { - t.Fatalf("wrong error: %s", ui.ErrorWriter) + t.Fatalf("wrong error: %s", done(t).Stderr()) } }) @@ -418,7 +451,7 @@ func TestInit_backendConfigFile(t *testing.T) { t.Fatalf("expected error, got success\n") } if !strings.Contains(done(t).All(), "Failed to read file") { - t.Fatalf("wrong error: %s", ui.ErrorWriter) + t.Fatalf("wrong error: %s", done(t).Stderr()) } }) @@ -518,7 +551,7 @@ func TestInit_backendReconfigure(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -541,7 +574,7 @@ func TestInit_backendReconfigure(t *testing.T) { args := []string{} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // now run init again, changing the path. @@ -549,7 +582,7 @@ func TestInit_backendReconfigure(t *testing.T) { // Without -reconfigure, the test fails since the backend asks for input on migrating state args = []string{"-reconfigure", "-backend-config", "path=changed"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } } @@ -560,7 +593,7 @@ func TestInit_backendConfigFileChange(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -571,7 +604,7 @@ func TestInit_backendConfigFileChange(t *testing.T) { args := []string{"-backend-config", "input.config", "-migrate-state"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // Read our saved backend config and verify we have our settings @@ -593,7 +626,7 @@ func TestInit_backendMigrateWhileLocked(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -624,13 +657,13 @@ func TestInit_backendMigrateWhileLocked(t *testing.T) { // Attempt to migrate args := []string{"-backend-config", "input.config", "-migrate-state", "-force-copy"} if code := c.Run(args); code == 0 { - t.Fatalf("expected nonzero exit code: %s", ui.OutputWriter.String()) + t.Fatalf("expected nonzero exit code: %s", done(t).Stdout()) } // Disabling locking should work args = []string{"-backend-config", "input.config", "-migrate-state", "-force-copy", "-lock=false"} if code := c.Run(args); code != 0 { - t.Fatalf("expected zero exit code, got %d: %s", code, ui.ErrorWriter.String()) + t.Fatalf("expected zero exit code, got %d: %s", code, done(t).Stderr()) } } @@ -678,7 +711,7 @@ func TestInit_backendConfigKV(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -689,7 +722,7 @@ func TestInit_backendConfigKV(t *testing.T) { args := []string{"-backend-config", "path=hello"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // Read our saved backend config and verify we have our settings @@ -706,7 +739,7 @@ func TestInit_backendConfigKVReInit(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -717,7 +750,7 @@ func TestInit_backendConfigKVReInit(t *testing.T) { args := []string{"-backend-config", "path=test"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } ui = new(cli.MockUi) @@ -732,7 +765,7 @@ func TestInit_backendConfigKVReInit(t *testing.T) { // a second init should require no changes, nor should it change the backend. args = []string{"-input=false"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // make sure the backend is configured how we expect @@ -748,7 +781,7 @@ func TestInit_backendConfigKVReInit(t *testing.T) { // override the -backend-config options by settings args = []string{"-input=false", "-backend-config", "", "-migrate-state"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // make sure the backend is configured how we expect @@ -769,7 +802,7 @@ func TestInit_backendConfigKVReInitWithConfigDiff(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -780,7 +813,7 @@ func TestInit_backendConfigKVReInitWithConfigDiff(t *testing.T) { args := []string{"-input=false"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } ui = new(cli.MockUi) @@ -796,7 +829,7 @@ func TestInit_backendConfigKVReInitWithConfigDiff(t *testing.T) { // should it change the backend. args = []string{"-input=false", "-backend-config", "path=foo"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // make sure the backend is configured how we expect @@ -828,7 +861,7 @@ func TestInit_backendCli_no_config_block(t *testing.T) { args := []string{"-backend-config", "path=test"} if code := c.Run(args); code != 0 { - t.Fatalf("got exit status %d; want 0\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + t.Fatalf("got exit status %d; want 0\nstderr:\n%s\n\nstdout:\n%s", code, done(t).Stderr(), done(t).Stdout()) } errMsg := done(t).All() @@ -856,7 +889,7 @@ func TestInit_backendReinitWithExtra(t *testing.T) { } ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -867,7 +900,7 @@ func TestInit_backendReinitWithExtra(t *testing.T) { args := []string{"-backend-config", "path=hello"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // Read our saved backend config and verify we have our settings @@ -882,7 +915,7 @@ func TestInit_backendReinitWithExtra(t *testing.T) { // init again and make sure nothing changes if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } state = testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) if got, want := normalizeJSON(t, state.Backend.ConfigRaw), `{"path":"hello","workspace_dir":null}`; got != want { @@ -900,7 +933,7 @@ func TestInit_backendReinitConfigToExtra(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -910,7 +943,7 @@ func TestInit_backendReinitConfigToExtra(t *testing.T) { } if code := c.Run([]string{"-input=false"}); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // Read our saved backend config and verify we have our settings @@ -939,7 +972,7 @@ func TestInit_backendReinitConfigToExtra(t *testing.T) { args := []string{"-input=false", "-backend-config=path=foo"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } state = testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) if got, want := normalizeJSON(t, state.Backend.ConfigRaw), `{"path":"foo","workspace_dir":null}`; got != want { @@ -1018,10 +1051,10 @@ func TestInit_backendCloudInvalidOptions(t *testing.T) { } args := []string{"-backend-config=anything"} if code := c.Run(args); code == 0 { - t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) + t.Fatalf("unexpected success\n%s", done(t).Stdout()) } - gotStderr := done(t).All() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option @@ -1056,10 +1089,10 @@ Cloud configuration block in the root module. } args := []string{"-reconfigure"} if code := c.Run(args); code == 0 { - t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) + t.Fatalf("unexpected success\n%s", done(t).Stdout()) } - gotStderr := done(t).All() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option @@ -1094,10 +1127,10 @@ Cloud configuration settings. } args := []string{"-reconfigure"} if code := c.Run(args); code == 0 { - t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) + t.Fatalf("unexpected success\n%s", done(t).Stdout()) } - gotStderr := done(t).All() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option @@ -1124,10 +1157,10 @@ because activating Terraform Cloud involves some additional steps. } args := []string{"-migrate-state"} if code := c.Run(args); code == 0 { - t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) + t.Fatalf("unexpected success\n%s", done(t).Stdout()) } - gotStderr := done(t).All() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option @@ -1162,10 +1195,10 @@ storage location is not configurable. } args := []string{"-migrate-state"} if code := c.Run(args); code == 0 { - t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) + t.Fatalf("unexpected success\n%s", done(t).Stdout()) } - gotStderr := done(t).All() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option @@ -1195,10 +1228,10 @@ prompts. } args := []string{"-force-copy"} if code := c.Run(args); code == 0 { - t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) + t.Fatalf("unexpected success\n%s", done(t).Stdout()) } - gotStderr := done(t).All() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option @@ -1232,11 +1265,13 @@ storage location is not configurable. }, } args := []string{"-force-copy"} - if code := c.Run(args); code == 0 { - t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code == 0 { + t.Fatalf("unexpected success\n%s", testOutput.Stdout()) } - gotStderr := done(t).All() + gotStderr := testOutput.Stderr() wantStderr := ` Error: Invalid command-line option @@ -1271,7 +1306,7 @@ func TestInit_inputFalse(t *testing.T) { args := []string{"-input=false", "-backend-config=path=foo"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // write different states for foo and bar @@ -1307,7 +1342,7 @@ func TestInit_inputFalse(t *testing.T) { args = []string{"-input=false", "-backend-config=path=bar", "-migrate-state"} if code := c.Run(args); code == 0 { - t.Fatal("init should have failed", ui.OutputWriter) + t.Fatal("init should have failed", done(t).Stdout()) } errMsg := done(t).All() @@ -1327,7 +1362,7 @@ func TestInit_inputFalse(t *testing.T) { // A missing input=false should abort rather than loop infinitely args = []string{"-backend-config=path=baz"} if code := c.Run(args); code == 0 { - t.Fatal("init should have failed", ui.OutputWriter) + t.Fatal("init should have failed", done(t).Stdout()) } } @@ -1339,7 +1374,7 @@ func TestInit_getProvider(t *testing.T) { overrides := metaOverridesForProvider(testProvider()) ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) providerSource, close := newMockProviderSource(t, map[string][]string{ // looking for an exact version "exact": {"1.2.3"}, @@ -1364,7 +1399,7 @@ func TestInit_getProvider(t *testing.T) { "-backend=false", // should be possible to install plugins without backend init } if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // check that we got the providers for our config @@ -1425,11 +1460,13 @@ func TestInit_getProvider(t *testing.T) { Meta: m, } - if code := c.Run(nil); code == 0 { - t.Fatal("expected error, got:", ui.OutputWriter) + code := c.Run(nil) + testOutput := done(t) + if code == 0 { + t.Fatal("expected error, got:", testOutput.Stdout()) } - errMsg := done(t).All() + errMsg := testOutput.Stderr() if !strings.Contains(errMsg, "Unsupported state file format") { t.Fatal("unexpected error:", errMsg) } @@ -1444,7 +1481,7 @@ func TestInit_getProviderSource(t *testing.T) { overrides := metaOverridesForProvider(testProvider()) ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) providerSource, close := newMockProviderSource(t, map[string][]string{ // looking for an exact version "acme/alpha": {"1.2.3"}, @@ -1468,7 +1505,7 @@ func TestInit_getProviderSource(t *testing.T) { "-backend=false", // should be possible to install plugins without backend init } if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // check that we got the providers for our config @@ -1509,9 +1546,10 @@ func TestInit_getProviderLegacyFromState(t *testing.T) { c := &InitCommand{ Meta: m, } - - if code := c.Run(nil); code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + code := c.Run(nil) + testOutput := done(t) + if code != 1 { + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, testOutput.Stderr(), testOutput.Stdout()) } // Expect this diagnostic output @@ -1519,7 +1557,7 @@ func TestInit_getProviderLegacyFromState(t *testing.T) { "Invalid legacy provider address", "You must complete the Terraform 0.13 upgrade process", } - got := done(t).All() + got := testOutput.All() for _, want := range wants { if !strings.Contains(got, want) { t.Fatalf("expected output to contain %q, got:\n\n%s", want, got) @@ -1567,8 +1605,10 @@ func TestInit_getProviderInvalidPackage(t *testing.T) { args := []string{ "-backend=false", // should be possible to install plugins without backend init } - if code := c.Run(args); code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code != 1 { + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, testOutput.Stderr(), testOutput.Stdout()) } // invalid provider should be installed @@ -1581,7 +1621,7 @@ func TestInit_getProviderInvalidPackage(t *testing.T) { "Failed to install provider", "could not find executable file starting with terraform-provider-package", } - got := done(t).All() + got := testOutput.All() for _, wantError := range wantErrors { if !strings.Contains(got, wantError) { t.Fatalf("missing error:\nwant: %q\ngot:\n%s", wantError, got) @@ -1626,8 +1666,10 @@ func TestInit_getProviderDetectedLegacy(t *testing.T) { args := []string{ "-backend=false", // should be possible to install plugins without backend init } - if code := c.Run(args); code == 0 { - t.Fatalf("expected error, got output: \n%s", ui.OutputWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code == 0 { + t.Fatalf("expected error, got output: \n%s", testOutput.Stdout()) } // foo should be installed @@ -1642,7 +1684,7 @@ func TestInit_getProviderDetectedLegacy(t *testing.T) { } // error output is the main focus of this test - errOutput := done(t).All() + errOutput := testOutput.All() errors := []string{ "Failed to query available provider packages", "Could not retrieve the list of available versions", @@ -1683,11 +1725,12 @@ func TestInit_providerSource(t *testing.T) { } args := []string{} - - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", testOutput.All()) } - if strings.Contains(ui.OutputWriter.String(), "Terraform has initialized, but configuration upgrades may be needed") { + if strings.Contains(testOutput.Stdout(), "Terraform has initialized, but configuration upgrades may be needed") { t.Fatalf("unexpected \"configuration upgrade\" warning in output") } @@ -1756,10 +1799,10 @@ func TestInit_providerSource(t *testing.T) { t.Errorf("wrong version selections after upgrade\n%s", diff) } - if got, want := ui.OutputWriter.String(), "Installed hashicorp/test v1.2.3 (verified checksum)"; !strings.Contains(got, want) { + if got, want := testOutput.Stdout(), "Installed hashicorp/test v1.2.3 (verified checksum)"; !strings.Contains(got, want) { t.Fatalf("unexpected output: %s\nexpected to include %q", got, want) } - if got, want := done(t).All(), "\n - hashicorp/source\n - hashicorp/test\n - hashicorp/test-beta"; !strings.Contains(got, want) { + if got, want := testOutput.All(), "\n - hashicorp/source\n - hashicorp/test\n - hashicorp/test-beta"; !strings.Contains(got, want) { t.Fatalf("wrong error message\nshould contain: %s\ngot:\n%s", want, got) } } @@ -1778,7 +1821,7 @@ func TestInit_cancelModules(t *testing.T) { close(shutdownCh) ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -1791,12 +1834,13 @@ func TestInit_cancelModules(t *testing.T) { } args := []string{} - - if code := c.Run(args); code == 0 { - t.Fatalf("succeeded; wanted error\n%s", ui.OutputWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code == 0 { + t.Fatalf("succeeded; wanted error\n%s", testOutput.Stdout()) } - if got, want := ui.ErrorWriter.String(), `Module installation was canceled by an interrupt signal`; !strings.Contains(got, want) { + if got, want := testOutput.Stderr(), `Module installation was canceled by an interrupt signal`; !strings.Contains(got, want) { t.Fatalf("wrong error message\nshould contain: %s\ngot:\n%s", want, got) } } @@ -1820,7 +1864,7 @@ func TestInit_cancelProviders(t *testing.T) { close(shutdownCh) ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -1834,15 +1878,16 @@ func TestInit_cancelProviders(t *testing.T) { } args := []string{} - - if code := c.Run(args); code == 0 { - t.Fatalf("succeeded; wanted error\n%s", ui.OutputWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code == 0 { + t.Fatalf("succeeded; wanted error\n%s", testOutput.All()) } // Currently the first operation that is cancelable is provider // installation, so our error message comes from there. If we // make the earlier steps cancelable in future then it'd be // expected for this particular message to change. - if got, want := ui.ErrorWriter.String(), `Provider installation was canceled by an interrupt signal`; !strings.Contains(got, want) { + if got, want := testOutput.Stderr(), `Provider installation was canceled by an interrupt signal`; !strings.Contains(got, want) { t.Fatalf("wrong error message\nshould contain: %s\ngot:\n%s", want, got) } } @@ -1864,7 +1909,7 @@ func TestInit_getUpgradePlugins(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -1885,7 +1930,7 @@ func TestInit_getUpgradePlugins(t *testing.T) { "-upgrade=true", } if code := c.Run(args); code != 0 { - t.Fatalf("command did not complete successfully:\n%s", ui.ErrorWriter.String()) + t.Fatalf("command did not complete successfully:\n%s", done(t).All()) } cacheDir := m.providerLocalCacheDir() @@ -2002,12 +2047,14 @@ func TestInit_getProviderMissing(t *testing.T) { } args := []string{} - if code := c.Run(args); code == 0 { - t.Fatalf("expected error, got output: \n%s", ui.OutputWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code == 0 { + t.Fatalf("expected error, got output: \n%s", testOutput.Stdout()) } - if !strings.Contains(done(t).All(), "no available releases match") { - t.Fatalf("unexpected error output: %s", ui.ErrorWriter) + if !strings.Contains(testOutput.All(), "no available releases match") { + t.Fatalf("unexpected error output: %s", testOutput.Stderr()) } } @@ -2029,7 +2076,7 @@ func TestInit_checkRequiredVersion(t *testing.T) { args := []string{} if code := c.Run(args); code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, done(t).All(), ui.OutputWriter.String()) + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, done(t).Stderr(), done(t).Stdout()) } errStr := done(t).All() if !strings.Contains(errStr, `required_version = "~> 0.9.0"`) { @@ -2060,7 +2107,7 @@ func TestInit_checkRequiredVersionFirst(t *testing.T) { args := []string{} if code := c.Run(args); code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, done(t).All(), ui.OutputWriter.String()) + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, done(t).Stderr(), done(t).Stdout()) } errStr := done(t).All() if !strings.Contains(errStr, `Unsupported Terraform Core version`) { @@ -2084,7 +2131,7 @@ func TestInit_checkRequiredVersionFirst(t *testing.T) { args := []string{} if code := c.Run(args); code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, done(t).All(), ui.OutputWriter.String()) + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, done(t).Stderr(), done(t).Stdout()) } errStr := done(t).All() if !strings.Contains(errStr, `Unsupported Terraform Core version`) { @@ -2121,7 +2168,7 @@ func TestInit_providerLockFile(t *testing.T) { args := []string{} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } lockFile := ".terraform.lock.hcl" @@ -2292,7 +2339,7 @@ provider "registry.terraform.io/hashicorp/test" { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -2312,10 +2359,10 @@ provider "registry.terraform.io/hashicorp/test" { code := c.Run(tc.args) if tc.ok && code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } if !tc.ok && code == 0 { - t.Fatalf("expected error, got output: \n%s", ui.OutputWriter.String()) + t.Fatalf("expected error, got output: \n%s", done(t).Stdout()) } buf, err := ioutil.ReadFile(lockFile) @@ -2340,7 +2387,7 @@ func TestInit_pluginDirReset(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -2361,7 +2408,7 @@ func TestInit_pluginDirReset(t *testing.T) { // run once and save the -plugin-dir args := []string{"-plugin-dir", "a"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter) + t.Fatalf("bad: \n%s", done(t).Stderr()) } pluginDirs, err := c.loadPluginPath() @@ -2386,7 +2433,7 @@ func TestInit_pluginDirReset(t *testing.T) { // make sure we remove the plugin-dir record args = []string{"-plugin-dir="} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter) + t.Fatalf("bad: \n%s", done(t).Stderr()) } pluginDirs, err = c.loadPluginPath() @@ -2410,7 +2457,7 @@ func TestInit_pluginDirProviders(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -2451,7 +2498,7 @@ func TestInit_pluginDirProviders(t *testing.T) { "-plugin-dir", "c", } if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter) + t.Fatalf("bad: \n%s", done(t).Stderr()) } locks, err := m.lockedDependencies() @@ -2549,15 +2596,17 @@ func TestInit_pluginDirProvidersDoesNotGet(t *testing.T) { "-plugin-dir", "a", "-plugin-dir", "b", } - if code := c.Run(args); code == 0 { + code := c.Run(args) + testOutput := done(t) + if code == 0 { // should have been an error - t.Fatalf("succeeded; want error\nstdout:\n%s\nstderr\n%s", ui.OutputWriter, ui.ErrorWriter) + t.Fatalf("succeeded; want error\nstdout:\n%s\nstderr\n%s", testOutput.Stdout(), testOutput.Stderr()) } // The error output should mention the "between" provider but should not // mention either the "exact" or "greater-than" provider, because the // latter two are available via the -plugin-dir directories. - errStr := done(t).All() + errStr := testOutput.Stderr() if subStr := "hashicorp/between"; !strings.Contains(errStr, subStr) { t.Errorf("error output should mention the 'between' provider\nwant substr: %s\ngot:\n%s", subStr, errStr) } @@ -2584,7 +2633,7 @@ func TestInit_pluginDirWithBuiltIn(t *testing.T) { defer close() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -2597,11 +2646,13 @@ func TestInit_pluginDirWithBuiltIn(t *testing.T) { } args := []string{"-plugin-dir", "./"} - if code := c.Run(args); code != 0 { - t.Fatalf("error: %s", ui.ErrorWriter) + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("error: %s", testOutput.Stderr()) } - outputStr := ui.OutputWriter.String() + outputStr := testOutput.Stdout() if subStr := "terraform.io/builtin/terraform is built in to Terraform"; !strings.Contains(outputStr, subStr) { t.Errorf("output should mention the terraform provider\nwant substr: %s\ngot:\n%s", subStr, outputStr) } @@ -2634,11 +2685,13 @@ func TestInit_invalidBuiltInProviders(t *testing.T) { Meta: m, } - if code := c.Run(nil); code == 0 { - t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) + code := c.Run(nil) + testOutput := done(t) + if code == 0 { + t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", testOutput.Stdout(), testOutput.Stderr()) } - errStr := done(t).All() + errStr := testOutput.Stderr() if subStr := "Cannot use terraform.io/builtin/terraform: built-in"; !strings.Contains(errStr, subStr) { t.Errorf("error output should mention the terraform provider\nwant substr: %s\ngot:\n%s", subStr, errStr) } @@ -2663,13 +2716,15 @@ func TestInit_invalidSyntaxNoBackend(t *testing.T) { Meta: m, } - if code := c.Run(nil); code == 0 { - t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) + code := c.Run(nil) + testOutput := done(t) + if code == 0 { + t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", testOutput.Stdout(), testOutput.Stderr()) } - errStr := done(t).All() - if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(ui.ErrorWriter.String(), subStr) { - t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, ui.ErrorWriter.String()) + errStr := testOutput.Stderr() + if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(errStr, subStr) { + t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, errStr) } if subStr := "Error: Unsupported block type"; !strings.Contains(errStr, subStr) { t.Errorf("Error output should mention the syntax problem\nwant substr: %s\ngot:\n%s", subStr, errStr) @@ -2692,13 +2747,15 @@ func TestInit_invalidSyntaxWithBackend(t *testing.T) { Meta: m, } - if code := c.Run(nil); code == 0 { - t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) + code := c.Run(nil) + testOutput := done(t) + if code == 0 { + t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", testOutput.Stdout(), testOutput.Stderr()) } - errStr := done(t).All() - if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(ui.ErrorWriter.String(), subStr) { - t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, ui.ErrorWriter.String()) + errStr := testOutput.Stderr() + if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(errStr, subStr) { + t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, errStr) } if subStr := "Error: Unsupported block type"; !strings.Contains(errStr, subStr) { t.Errorf("Error output should mention the syntax problem\nwant substr: %s\ngot:\n%s", subStr, errStr) @@ -2721,13 +2778,15 @@ func TestInit_invalidSyntaxInvalidBackend(t *testing.T) { Meta: m, } - if code := c.Run(nil); code == 0 { - t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) + code := c.Run(nil) + testOutput := done(t) + if code == 0 { + t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", testOutput.Stdout(), testOutput.Stderr()) } - errStr := done(t).All() - if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(ui.ErrorWriter.String(), subStr) { - t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, ui.ErrorWriter.String()) + errStr := testOutput.Stderr() + if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(errStr, subStr) { + t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, errStr) } if subStr := "Error: Unsupported block type"; !strings.Contains(errStr, subStr) { t.Errorf("Error output should mention syntax errors\nwant substr: %s\ngot:\n%s", subStr, errStr) @@ -2753,13 +2812,15 @@ func TestInit_invalidSyntaxBackendAttribute(t *testing.T) { Meta: m, } - if code := c.Run(nil); code == 0 { - t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) + code := c.Run(nil) + testOutput := done(t) + if code == 0 { + t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", testOutput.Stdout(), testOutput.Stderr()) } - errStr := done(t).All() - if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(ui.ErrorWriter.String(), subStr) { - t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, ui.ErrorWriter.String()) + errStr := testOutput.All() + if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(errStr, subStr) { + t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, errStr) } if subStr := "Error: Invalid character"; !strings.Contains(errStr, subStr) { t.Errorf("Error output should mention the invalid character\nwant substr: %s\ngot:\n%s", subStr, errStr) @@ -2783,7 +2844,7 @@ func TestInit_tests(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(provider), @@ -2795,7 +2856,7 @@ func TestInit_tests(t *testing.T) { args := []string{} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } } @@ -2824,11 +2885,13 @@ func TestInit_testsWithProvider(t *testing.T) { } args := []string{} - if code := c.Run(args); code == 0 { - t.Fatalf("expected failure but got: \n%s", ui.OutputWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code == 0 { + t.Fatalf("expected failure but got: \n%s", testOutput.All()) } - got := done(t).All() + got := testOutput.Stderr() want := ` Error: Failed to query available provider packages @@ -2841,38 +2904,6 @@ hashicorp/test: no available releases match the given constraints 1.0.1, } } -func TestInit_jsonTestsWithProvider(t *testing.T) { - // Create a temporary working directory that is empty - td := t.TempDir() - testCopyDir(t, testFixturePath("init-with-tests-with-provider"), td) - defer testChdir(t, td)() - - provider := applyFixtureProvider() // We just want the types from this provider. - - providerSource, close := newMockProviderSource(t, map[string][]string{ - "hashicorp/test": {"1.0.0"}, - }) - defer close() - - ui := new(cli.MockUi) - view, done := testView(t) - c := &InitCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(provider), - Ui: ui, - View: view, - ProviderSource: providerSource, - }, - } - - args := []string{"-json"} - if code := c.Run(args); code == 0 { - t.Fatalf("expected failure but got: \n%s", ui.OutputWriter.String()) - } - - checkGoldenReference(t, done(t), "init-with-tests-with-provider") -} - func TestInit_testsWithModule(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() @@ -2887,7 +2918,7 @@ func TestInit_testsWithModule(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(provider), @@ -2898,12 +2929,14 @@ func TestInit_testsWithModule(t *testing.T) { } args := []string{} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", testOutput.All()) } // Check output - output := ui.OutputWriter.String() + output := testOutput.Stdout() if !strings.Contains(output, "test.main.setup in setup") { t.Fatalf("doesn't look like we installed the test module': %s", output) } diff --git a/internal/command/providers_schema_test.go b/internal/command/providers_schema_test.go index bcbd48a65cee..235f1094338f 100644 --- a/internal/command/providers_schema_test.go +++ b/internal/command/providers_schema_test.go @@ -59,7 +59,7 @@ func TestProvidersSchema_output(t *testing.T) { p := providersSchemaFixtureProvider() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, @@ -72,12 +72,9 @@ func TestProvidersSchema_output(t *testing.T) { Meta: m, } if code := ic.Run([]string{}); code != 0 { - t.Fatalf("init failed\n%s", ui.ErrorWriter) + t.Fatalf("init failed\n%s", done(t).Stderr()) } - // flush the init output from the mock ui - ui.OutputWriter.Reset() - // `terraform provider schemas` command pc := &ProvidersSchemaCommand{Meta: m} if code := pc.Run([]string{"-json"}); code != 0 { diff --git a/internal/command/test_test.go b/internal/command/test_test.go index a1f4f4dca56f..95795fb234ab 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -1710,7 +1710,21 @@ func TestTest_SensitiveInputValues(t *testing.T) { t.Errorf("expected status code 0 but got %d", code) } - expected := `main.tftest.hcl... in progress + expected := `Initializing the backend... +Initializing modules... +- test.main.setup in setup +Initializing provider plugins... + +Terraform has been successfully initialized! + +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. +main.tftest.hcl... in progress run "setup"... pass run "test"... pass @@ -1915,7 +1929,20 @@ func TestTest_InvalidOverrides(t *testing.T) { t.Errorf("expected status code 0 but got %d", code) } - expected := ` + expected := `Initializing the backend... +Initializing modules... +- setup in setup +- test.main.setup in setup +Initializing provider plugins... +- Finding latest version of hashicorp/test... +- Installing hashicorp/test v1.0.0... +- Installed hashicorp/test v1.0.0 (verified checksum) +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. + + Warning: Incomplete lock file information for providers Due to your customized provider installation methods, Terraform was forced to @@ -1929,6 +1956,15 @@ providers. To calculate additional checksums for another platform, run: terraform providers lock -platform=linux_amd64 (where linux_amd64 is the platform to generate) +Terraform has been successfully initialized! + +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. main.tftest.hcl... in progress run "setup"... pass @@ -2023,7 +2059,19 @@ func TestTest_RunBlocksInProviders(t *testing.T) { t.Errorf("expected status code 0 but got %d", code) } - expected := ` + expected := `Initializing the backend... +Initializing modules... +- test.main.setup in setup +Initializing provider plugins... +- Finding latest version of hashicorp/test... +- Installing hashicorp/test v1.0.0... +- Installed hashicorp/test v1.0.0 (verified checksum) +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. + + Warning: Incomplete lock file information for providers Due to your customized provider installation methods, Terraform was forced to @@ -2037,6 +2085,15 @@ providers. To calculate additional checksums for another platform, run: terraform providers lock -platform=linux_amd64 (where linux_amd64 is the platform to generate) +Terraform has been successfully initialized! + +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. main.tftest.hcl... in progress run "setup"... pass run "main"... pass @@ -2098,7 +2155,17 @@ func TestTest_RunBlocksInProviders_BadReferences(t *testing.T) { t.Errorf("expected status code 1 but got %d", code) } - expectedOut := ` + expectedOut := `Initializing the backend... +Initializing provider plugins... +- Finding latest version of hashicorp/test... +- Installing hashicorp/test v1.0.0... +- Installed hashicorp/test v1.0.0 (verified checksum) +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. + + Warning: Incomplete lock file information for providers Due to your customized provider installation methods, Terraform was forced to @@ -2112,6 +2179,15 @@ providers. To calculate additional checksums for another platform, run: terraform providers lock -platform=linux_amd64 (where linux_amd64 is the platform to generate) +Terraform has been successfully initialized! + +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. missing_run_block.tftest.hcl... in progress run "main"... fail missing_run_block.tftest.hcl... tearing down diff --git a/internal/command/testdata/init-get/output.jsonlog b/internal/command/testdata/init-get/output.jsonlog new file mode 100644 index 000000000000..642606d7704b --- /dev/null +++ b/internal/command/testdata/init-get/output.jsonlog @@ -0,0 +1,8 @@ +{"@level":"info","@message":"Terraform 1.9.0-dev","@module":"terraform.ui","terraform":"1.9.0-dev","type":"version","ui":"1.2"} +{"@level":"info","@message":"Initializing the backend...","@module":"terraform.ui","type":"init_output"} +{"@level":"info","@message":"Initializing modules...","@module":"terraform.ui","type":"init_output"} +{"@level":"info","@message":"- foo in foo","@module":"terraform.ui","type":"log"} +{"@level":"info","@message":"Initializing provider plugins...","@module":"terraform.ui","type":"init_output"} +{"@level":"info","@message":"","@module":"terraform.ui","type":"init_output"} +{"@level":"info","@message":"Terraform has been successfully initialized!","@module":"terraform.ui","type":"init_output"} +{"@level":"info","@message":"You may now begin working with Terraform. Try running \"terraform plan\" to see\nany changes that are required for your infrastructure. All Terraform commands\nshould now work.\n\nIf you ever set or change modules or backend configuration for Terraform,\nrerun this command to reinitialize your working directory. If you forget, other\ncommands will detect it and remind you to do so if necessary.","@module":"terraform.ui","type":"init_output"} diff --git a/internal/command/testdata/init-migrate-state-with-json/hello.tf b/internal/command/testdata/init-migrate-state-with-json/hello.tf new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/internal/command/testdata/init-migrate-state-with-json/output.jsonlog b/internal/command/testdata/init-migrate-state-with-json/output.jsonlog new file mode 100644 index 000000000000..1f52cb38de68 --- /dev/null +++ b/internal/command/testdata/init-migrate-state-with-json/output.jsonlog @@ -0,0 +1,2 @@ +{"@level":"info","@message":"Terraform 1.9.0-dev","@module":"terraform.ui","terraform":"1.9.0-dev","type":"version","ui":"1.2"} +{"@level":"error","@message":"Error: The -migrate-state and -json options are mutually-exclusive","@module":"terraform.ui","diagnostic":{"severity":"error","summary":"The -migrate-state and -json options are mutually-exclusive","detail":"Terraform cannot ask for interactive approval when -json is set. To use the -migrate-state option, disable the -json option."},"type":"diagnostic"} diff --git a/internal/command/testdata/init-with-tests-with-provider/output.jsonlog b/internal/command/testdata/init-with-tests-with-provider/output.jsonlog deleted file mode 100644 index aad930f3fedf..000000000000 --- a/internal/command/testdata/init-with-tests-with-provider/output.jsonlog +++ /dev/null @@ -1,2 +0,0 @@ -{"@level":"info","@message":"Terraform 1.9.0-dev","@module":"terraform.ui","terraform":"1.9.0-dev","type":"version","ui":"1.2"} -{"@level":"error","@message":"Error: Failed to query available provider packages","@module":"terraform.ui","diagnostic":{"severity":"error","summary":"Failed to query available provider packages","detail":"Could not retrieve the list of available versions for provider hashicorp/test: no available releases match the given constraints 1.0.1, 1.0.2"},"type":"diagnostic"} \ No newline at end of file diff --git a/internal/command/validate_test.go b/internal/command/validate_test.go index d7a7e94ff068..b3ae34bcfe65 100644 --- a/internal/command/validate_test.go +++ b/internal/command/validate_test.go @@ -361,7 +361,20 @@ func TestValidateWithInvalidOverrides(t *testing.T) { } actual := output.All() - expected := ` + expected := `Initializing the backend... +Initializing modules... +- setup in setup +- test.main.setup in setup +Initializing provider plugins... +- Finding latest version of hashicorp/test... +- Installing hashicorp/test v1.0.0... +- Installed hashicorp/test v1.0.0 (verified checksum) +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. + + Warning: Incomplete lock file information for providers Due to your customized provider installation methods, Terraform was forced to @@ -375,6 +388,15 @@ providers. To calculate additional checksums for another platform, run: terraform providers lock -platform=linux_amd64 (where linux_amd64 is the platform to generate) +Terraform has been successfully initialized! + +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. Warning: Invalid override target diff --git a/internal/command/views/init.go b/internal/command/views/init.go index a53a80a28201..97076b813bcf 100644 --- a/internal/command/views/init.go +++ b/internal/command/views/init.go @@ -85,7 +85,7 @@ func (v *InitJSON) Diagnostics(diags tfdiags.Diagnostics) { } func (v *InitJSON) Output(messageCode string, params ...any) { - current_timestamp := time.Now().Format(time.RFC3339) + current_timestamp := time.Now().UTC().Format(time.RFC3339) json_data := map[string]string{ "@level": "info", @@ -315,49 +315,6 @@ 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 diff --git a/internal/command/views/init_test.go b/internal/command/views/init_test.go index 6b293ed97aa9..fb84d6fad8c6 100644 --- a/internal/command/views/init_test.go +++ b/internal/command/views/init_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/google/go-cmp/cmp" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/terminal" @@ -15,7 +16,7 @@ import ( tfversion "github.com/hashicorp/terraform/version" ) -func TestNewInit_jsonView(t *testing.T) { +func TestNewInit_jsonViewDiagnostics(t *testing.T) { streams, done := terminal.StreamsForTesting(t) newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) @@ -64,7 +65,7 @@ func TestNewInit_jsonView(t *testing.T) { testJSONViewOutputEqualsFull(t, actual, want) } -func TestNewInit_humanView(t *testing.T) { +func TestNewInit_humanViewDiagnostics(t *testing.T) { streams, done := terminal.StreamsForTesting(t) newInit := NewInit(arguments.ViewHuman, NewView(streams).SetRunningInAutomation(true)) @@ -82,7 +83,7 @@ func TestNewInit_humanView(t *testing.T) { } } -func TestNewInit_unsupportedView(t *testing.T) { +func TestNewInit_unsupportedViewDiagnostics(t *testing.T) { defer func() { r := recover() if r == nil { @@ -118,3 +119,233 @@ func getTestDiags(t *testing.T) tfdiags.Diagnostics { return diags } + +func TestNewInit_jsonViewOutput(t *testing.T) { + t.Run("no param", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitJSON); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + messageCode := "initializing_provider_plugin_message" + newInit.Output(messageCode) + + version := tfversion.String() + want := []map[string]interface{}{ + { + "@level": "info", + "@message": fmt.Sprintf("Terraform %s", version), + "@module": "terraform.ui", + "terraform": version, + "type": "version", + "ui": JSON_UI_VERSION, + }, + { + "@level": "info", + "@message": "Initializing provider plugins...", + "@module": "terraform.ui", + "type": "init_output", + }, + } + + actual := done(t).Stdout() + testJSONViewOutputEqualsFull(t, actual, want) + }) + + t.Run("single param", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitJSON); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + packageName := "hashicorp/aws" + messageCode := "finding_latest_version_message" + newInit.Output(messageCode, packageName) + + version := tfversion.String() + want := []map[string]interface{}{ + { + "@level": "info", + "@message": fmt.Sprintf("Terraform %s", version), + "@module": "terraform.ui", + "terraform": version, + "type": "version", + "ui": JSON_UI_VERSION, + }, + { + "@level": "info", + "@message": fmt.Sprintf("- Finding latest version of %s...", packageName), + "@module": "terraform.ui", + "type": "init_output", + }, + } + + actual := done(t).Stdout() + testJSONViewOutputEqualsFull(t, actual, want) + }) + + t.Run("variable length params", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitJSON); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + var packageName, packageVersion = "hashicorp/aws", "3.0.0" + messageCode := "provider_already_installed_message" + newInit.Output(messageCode, packageName, packageVersion) + + version := tfversion.String() + want := []map[string]interface{}{ + { + "@level": "info", + "@message": fmt.Sprintf("Terraform %s", version), + "@module": "terraform.ui", + "terraform": version, + "type": "version", + "ui": JSON_UI_VERSION, + }, + { + "@level": "info", + "@message": fmt.Sprintf("- Using previously-installed %s v%s", packageName, packageVersion), + "@module": "terraform.ui", + "type": "init_output", + }, + } + + actual := done(t).Stdout() + testJSONViewOutputEqualsFull(t, actual, want) + }) +} + +func TestNewInit_jsonViewLog(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitJSON); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + messageCode := "initializing_provider_plugin_message" + newInit.Log(messageCode) + + version := tfversion.String() + want := []map[string]interface{}{ + { + "@level": "info", + "@message": fmt.Sprintf("Terraform %s", version), + "@module": "terraform.ui", + "terraform": version, + "type": "version", + "ui": JSON_UI_VERSION, + }, + { + "@level": "info", + "@message": "Initializing provider plugins...", + "@module": "terraform.ui", + "type": "log", + }, + } + + actual := done(t).Stdout() + testJSONViewOutputEqualsFull(t, actual, want) +} + +func TestNewInit_jsonViewPrepareMessage(t *testing.T) { + t.Run("message code that does not exists", func(t *testing.T) { + streams, _ := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitJSON); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + messageCode := "Terraform has been successfully initialized!" + want := messageCode + + actual := newInit.PrepareMessage(messageCode) + if !cmp.Equal(want, actual) { + t.Errorf("unexpected output: %s", cmp.Diff(want, actual)) + } + }) + + t.Run("existing message code", func(t *testing.T) { + streams, _ := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitJSON); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + messageCode := "initializing_modules_message" + want := "Initializing modules..." + + actual := newInit.PrepareMessage(messageCode) + if !cmp.Equal(want, actual) { + t.Errorf("unexpected output: %s", cmp.Diff(want, actual)) + } + }) +} + +func TestNewInit_humanViewOutput(t *testing.T) { + t.Run("no param", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewHuman, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitHuman); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + messageCode := "initializing_provider_plugin_message" + newInit.Output(messageCode) + + actual := done(t).All() + expected := "Initializing provider plugins..." + if !strings.Contains(actual, expected) { + t.Fatalf("expected output to contain: %s, but got %s", expected, actual) + } + }) + + t.Run("single param", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewHuman, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitHuman); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + packageName := "hashicorp/aws" + messageCode := "finding_latest_version_message" + newInit.Output(messageCode, packageName) + + actual := done(t).All() + expected := "Finding latest version of hashicorp/aws" + if !strings.Contains(actual, expected) { + t.Fatalf("expected output to contain: %s, but got %s", expected, actual) + } + }) + + t.Run("variable length params", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewHuman, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitHuman); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + var packageName, packageVersion = "hashicorp/aws", "3.0.0" + messageCode := "provider_already_installed_message" + newInit.Output(messageCode, packageName, packageVersion) + + actual := done(t).All() + expected := "- Using previously-installed hashicorp/aws v3.0.0" + if !strings.Contains(actual, expected) { + t.Fatalf("expected output to contain: %s, but got %s", expected, actual) + } + }) +} diff --git a/internal/command/views/json_view_test.go b/internal/command/views/json_view_test.go index 4f84b8a6977e..ac12410f7119 100644 --- a/internal/command/views/json_view_test.go +++ b/internal/command/views/json_view_test.go @@ -410,7 +410,7 @@ func testJSONViewOutputEqualsFull(t *testing.T, output string, want []map[string delete(gotStruct, "@timestamp") // Verify the timestamp format - if _, err := time.Parse("2006-01-02T15:04:05.000000Z07:00", timestamp.(string)); err != nil { + if _, err := time.Parse(time.RFC3339, timestamp.(string)); err != nil { t.Errorf("error parsing timestamp on line %d: %s", i, err) } } From ce7824f90549994ec1389283dba32af6af6e7763 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Wed, 10 Apr 2024 14:16:22 -0400 Subject: [PATCH 09/13] add interrupt error to the diags so each component becomes responsible for rendering the errors --- internal/command/meta_config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/command/meta_config.go b/internal/command/meta_config.go index eb282cd66355..917d9d08a834 100644 --- a/internal/command/meta_config.go +++ b/internal/command/meta_config.go @@ -208,7 +208,7 @@ func (m *Meta) installModules(ctx context.Context, rootDir, testsDir string, upg if ctx.Err() == context.Canceled { m.showDiagnostics(diags) - m.Ui.Error("Module installation was canceled by an interrupt signal.") + diags = diags.Append(fmt.Errorf("Module installation was canceled by an interrupt signal.")) return true, diags } @@ -241,7 +241,7 @@ func (m *Meta) initDirFromModule(ctx context.Context, targetDir string, addr str diags = diags.Append(moreDiags) if ctx.Err() == context.Canceled { m.showDiagnostics(diags) - m.Ui.Error("Module initialization was canceled by an interrupt signal.") + diags = diags.Append(fmt.Errorf("Module initialization was canceled by an interrupt signal.")) return true, diags } return false, diags From c7bbc09631783c6aaecdacfc58a135ced6bc0129 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Mon, 15 Apr 2024 19:28:30 -0400 Subject: [PATCH 10/13] move all command args from command/init to args/init --- internal/command/apply.go | 4 +- internal/command/arguments/extended.go | 12 +- internal/command/arguments/flags.go | 60 +++++----- internal/command/arguments/init.go | 49 +++++++- internal/command/arguments/init_test.go | 12 +- internal/command/arguments/test.go | 2 +- internal/command/flag_kv.go | 13 -- internal/command/init.go | 83 +++++-------- internal/command/init_test.go | 3 +- internal/command/meta.go | 8 +- internal/command/meta_config.go | 57 --------- internal/command/meta_vars.go | 10 +- internal/command/plan.go | 4 +- internal/command/providers_lock.go | 3 +- internal/command/providers_mirror.go | 3 +- internal/command/refresh.go | 4 +- internal/command/test.go | 26 ++-- .../command/testdata/init-get/output.jsonlog | 1 - internal/command/views/init.go | 113 +++++++++++------- internal/command/views/init_test.go | 41 ++----- 20 files changed, 223 insertions(+), 285 deletions(-) diff --git a/internal/command/apply.go b/internal/command/apply.go index e332f94b47e7..1223e3d3b493 100644 --- a/internal/command/apply.go +++ b/internal/command/apply.go @@ -319,12 +319,12 @@ func (c *ApplyCommand) GatherVariables(opReq *backendrun.Operation, args *argume // package directly, removing this shim layer. varArgs := args.All() - items := make([]rawFlag, len(varArgs)) + 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 = rawFlags{items: &items} + c.Meta.variableArgs = arguments.FlagNameValueSlice{Items: &items} opReq.Variables, diags = c.collectVariableValues() return diags diff --git a/internal/command/arguments/extended.go b/internal/command/arguments/extended.go index 6e9aadea2a25..3b3c7cc759f9 100644 --- a/internal/command/arguments/extended.go +++ b/internal/command/arguments/extended.go @@ -192,13 +192,13 @@ func (o *Operation) Parse() tfdiags.Diagnostics { } // Vars describes arguments which specify non-default variable values. This -// interfce is unfortunately obscure, because the order of the CLI arguments +// interface is unfortunately obscure, because the order of the CLI arguments // determines the final value of the gathered variables. In future it might be // desirable for the arguments package to handle the gathering of variables // directly, returning a map of variable values. type Vars struct { - vars *flagNameValueSlice - varFiles *flagNameValueSlice + vars *FlagNameValueSlice + varFiles *FlagNameValueSlice } func (v *Vars) All() []FlagNameValue { @@ -239,14 +239,14 @@ func extendedFlagSet(name string, state *State, operation *Operation, vars *Vars f.BoolVar(&operation.Refresh, "refresh", true, "refresh") f.BoolVar(&operation.destroyRaw, "destroy", false, "destroy") f.BoolVar(&operation.refreshOnlyRaw, "refresh-only", false, "refresh-only") - f.Var((*flagStringSlice)(&operation.targetsRaw), "target", "target") - f.Var((*flagStringSlice)(&operation.forceReplaceRaw), "replace", "replace") + f.Var((*FlagStringSlice)(&operation.targetsRaw), "target", "target") + f.Var((*FlagStringSlice)(&operation.forceReplaceRaw), "replace", "replace") } // Gather all -var and -var-file arguments into one heterogenous structure // to preserve the overall order. if vars != nil { - varsFlags := newFlagNameValueSlice("-var") + varsFlags := NewFlagNameValueSlice("-var") varFilesFlags := varsFlags.Alias("-var-file") vars.vars = &varsFlags vars.varFiles = &varFilesFlags diff --git a/internal/command/arguments/flags.go b/internal/command/arguments/flags.go index 7a19a544eed9..64bf18ddd900 100644 --- a/internal/command/arguments/flags.go +++ b/internal/command/arguments/flags.go @@ -8,72 +8,68 @@ import ( "fmt" ) -// flagStringSlice is a flag.Value implementation which allows collecting +// FlagStringSlice is a flag.Value implementation which allows collecting // multiple instances of a single flag into a slice. This is used for flags // such as -target=aws_instance.foo and -var x=y. -type flagStringSlice []string +type FlagStringSlice []string -var _ flag.Value = (*flagStringSlice)(nil) +var _ flag.Value = (*FlagStringSlice)(nil) -func (v *flagStringSlice) String() string { +func (v *FlagStringSlice) String() string { return "" } -func (v *flagStringSlice) Set(raw string) error { +func (v *FlagStringSlice) Set(raw string) error { *v = append(*v, raw) return nil } -// flagNameValueSlice is a flag.Value implementation that appends raw flag +// FlagNameValueSlice is a flag.Value implementation that appends raw flag // names and values to a slice. This is used to collect a sequence of flags // with possibly different names, preserving the overall order. -// -// FIXME: this is a copy of rawFlags from command/meta_config.go, with the -// eventual aim of replacing it altogether by gathering variables in the -// arguments package. -type flagNameValueSlice struct { - flagName string - items *[]FlagNameValue +type FlagNameValueSlice struct { + FlagName string + Items *[]FlagNameValue } -var _ flag.Value = flagNameValueSlice{} +var _ flag.Value = FlagNameValueSlice{} -func newFlagNameValueSlice(flagName string) flagNameValueSlice { +func NewFlagNameValueSlice(flagName string) FlagNameValueSlice { var items []FlagNameValue - return flagNameValueSlice{ - flagName: flagName, - items: &items, + return FlagNameValueSlice{ + FlagName: flagName, + Items: &items, } } -func (f flagNameValueSlice) Empty() bool { - if f.items == nil { +func (f FlagNameValueSlice) Empty() bool { + if f.Items == nil { return true } - return len(*f.items) == 0 + return len(*f.Items) == 0 } -func (f flagNameValueSlice) AllItems() []FlagNameValue { - if f.items == nil { +func (f FlagNameValueSlice) AllItems() []FlagNameValue { + if f.Items == nil { return nil } - return *f.items + return *f.Items } -func (f flagNameValueSlice) Alias(flagName string) flagNameValueSlice { - return flagNameValueSlice{ - flagName: flagName, - items: f.items, +func (f FlagNameValueSlice) Alias(flagName string) FlagNameValueSlice { + return FlagNameValueSlice{ + FlagName: flagName, + Items: f.Items, } } -func (f flagNameValueSlice) String() string { +func (f FlagNameValueSlice) String() string { return "" } -func (f flagNameValueSlice) Set(str string) error { - *f.items = append(*f.items, FlagNameValue{ - Name: f.flagName, +func (f FlagNameValueSlice) Set(str string) error { + *f.Items = append(*f.Items, FlagNameValue{ + Name: f.FlagName, Value: str, }) return nil diff --git a/internal/command/arguments/init.go b/internal/command/arguments/init.go index fc4a702769d9..b6ae8f98c2aa 100644 --- a/internal/command/arguments/init.go +++ b/internal/command/arguments/init.go @@ -4,7 +4,6 @@ package arguments import ( - "flag" "time" "github.com/hashicorp/terraform/internal/tfdiags" @@ -58,15 +57,39 @@ type Init struct { // IgnoreRemoteVersion specifies whether to ignore remote and local Terraform versions compatibility IgnoreRemoteVersion bool + + BackendConfig FlagNameValueSlice + + Vars *Vars + + // InputEnabled is used to disable interactive input for unspecified + // variable and backend config values. Default is true. + InputEnabled bool + + TargetFlags []string + + CompactWarnings bool + + PluginPath FlagStringSlice + + Args []string } // 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) { +func ParseInit(args []string) (*Init, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - init := &Init{} + init := &Init{ + Vars: &Vars{}, + } + init.BackendConfig = NewFlagNameValueSlice("-backend-config") + + cmdFlags := extendedFlagSet("init", nil, nil, init.Vars) + cmdFlags.Var((*FlagStringSlice)(&init.TargetFlags), "target", "resource to target") + cmdFlags.BoolVar(&init.InputEnabled, "input", true, "input") + cmdFlags.BoolVar(&init.CompactWarnings, "compact-warnings", false, "use compact warnings") 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") @@ -81,6 +104,8 @@ func ParseInit(args []string, cmdFlags *flag.FlagSet) (*Init, tfdiags.Diagnostic 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") + cmdFlags.Var(&init.BackendConfig, "backend-config", "") + cmdFlags.Var(&init.PluginPath, "plugin-dir", "plugin directory") if err := cmdFlags.Parse(args); err != nil { diags = diags.Append(tfdiags.Sourceless( @@ -90,6 +115,24 @@ func ParseInit(args []string, cmdFlags *flag.FlagSet) (*Init, tfdiags.Diagnostic )) } + if init.MigrateState && init.Json { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "The -migrate-state and -json options are mutually-exclusive", + "Terraform cannot ask for interactive approval when -json is set. To use the -migrate-state option, disable the -json option.", + )) + } + + if init.MigrateState && init.Reconfigure { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid init options", + "The -migrate-state and -reconfigure options are mutually-exclusive.", + )) + } + + init.Args = cmdFlags.Args() + backendFlagSet := FlagIsSet(cmdFlags, "backend") cloudFlagSet := FlagIsSet(cmdFlags, "cloud") diff --git a/internal/command/arguments/init_test.go b/internal/command/arguments/init_test.go index 6dacbf6eff2e..97f7c76a34c2 100644 --- a/internal/command/arguments/init_test.go +++ b/internal/command/arguments/init_test.go @@ -4,8 +4,6 @@ package arguments import ( - "flag" - "io" "strings" "testing" "time" @@ -86,10 +84,7 @@ func TestParseInit_basicValid(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { - cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError) - cmdFlags.SetOutput(io.Discard) - - got, diags := ParseInit(tc.args, cmdFlags) + got, diags := ParseInit(tc.args) if len(diags) > 0 { t.Fatalf("unexpected diags: %v", diags) } @@ -118,10 +113,7 @@ func TestParseInit_invalid(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { - cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError) - cmdFlags.SetOutput(io.Discard) - - got, diags := ParseInit(tc.args, cmdFlags) + got, diags := ParseInit(tc.args) if len(diags) == 0 { t.Fatal("expected diags but got none") } diff --git a/internal/command/arguments/test.go b/internal/command/arguments/test.go index 51e2ab604f3b..e9168aff11e9 100644 --- a/internal/command/arguments/test.go +++ b/internal/command/arguments/test.go @@ -50,7 +50,7 @@ func ParseTest(args []string) (*Test, tfdiags.Diagnostics) { var jsonOutput bool cmdFlags := extendedFlagSet("test", nil, nil, test.Vars) - cmdFlags.Var((*flagStringSlice)(&test.Filter), "filter", "filter") + cmdFlags.Var((*FlagStringSlice)(&test.Filter), "filter", "filter") cmdFlags.StringVar(&test.TestDirectory, "test-directory", configs.DefaultTestDirectory, "test-directory") cmdFlags.BoolVar(&jsonOutput, "json", false, "json") cmdFlags.StringVar(&test.JUnitXMLFile, "junit-xml", "", "junit-xml") diff --git a/internal/command/flag_kv.go b/internal/command/flag_kv.go index cf351d2f4a08..2d16ca5505ce 100644 --- a/internal/command/flag_kv.go +++ b/internal/command/flag_kv.go @@ -31,16 +31,3 @@ func (v *FlagStringKV) Set(raw string) error { (*v)[key] = value return nil } - -// FlagStringSlice is a flag.Value implementation for parsing targets from the -// command line, e.g. -target=aws_instance.foo -target=aws_vpc.bar -type FlagStringSlice []string - -func (v *FlagStringSlice) String() string { - return "" -} -func (v *FlagStringSlice) Set(raw string) error { - *v = append(*v, raw) - - return nil -} diff --git a/internal/command/init.go b/internal/command/init.go index 83800452ecf2..c2b7bb4cc7a7 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -43,24 +43,9 @@ type InitCommand struct { } func (c *InitCommand) Run(args []string) int { - var flagPluginPath FlagStringSlice - flagConfigExtra := newRawFlags("-backend-config") - var diags tfdiags.Diagnostics args = c.Meta.process(args) - cmdFlags := c.Meta.extendedFlagSet("init") - cmdFlags.Usage = func() { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Failed to parse command-line flags", - c.Help(), - )) - } - - cmdFlags.Var(flagConfigExtra, "backend-config", "") - cmdFlags.Var(&flagPluginPath, "plugin-dir", "plugin directory") - - initArgs, initDiags := arguments.ParseInit(args, cmdFlags) + initArgs, initDiags := arguments.ParseInit(args) view := views.NewInit(initArgs.ViewType, c.View) @@ -76,26 +61,17 @@ func (c *InitCommand) Run(args []string) int { c.reconfigure = initArgs.Reconfigure c.migrateState = initArgs.MigrateState c.Meta.ignoreRemoteVersion = initArgs.IgnoreRemoteVersion - - if initArgs.MigrateState && initArgs.Json { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "The -migrate-state and -json options are mutually-exclusive", - "Terraform cannot ask for interactive approval when -json is set. To use the -migrate-state option, disable the -json option.", - )) - view.Diagnostics(diags) - return 1 - } - - if c.migrateState && c.reconfigure { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Invalid init options", - "The -migrate-state and -reconfigure options are mutually-exclusive", - )) - view.Diagnostics(diags) - return 1 + 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 @@ -103,13 +79,12 @@ func (c *InitCommand) Run(args []string) int { c.migrateState = true } - if len(flagPluginPath) > 0 { - c.pluginPath = flagPluginPath + if len(initArgs.PluginPath) > 0 { + c.pluginPath = initArgs.PluginPath } // Validate the arg count and get the working directory - args = cmdFlags.Args() - path, err := ModulePath(args) + path, err := ModulePath(initArgs.Args) if err != nil { diags = diags.Append(err) view.Diagnostics(diags) @@ -207,9 +182,9 @@ func (c *InitCommand) Run(args []string) int { switch { case initArgs.Cloud && rootModEarly.CloudConfig != nil: - back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, flagConfigExtra, initArgs.ViewType, view) + back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view) case initArgs.Backend: - back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, flagConfigExtra, initArgs.ViewType, view) + 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) @@ -317,7 +292,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, initArgs.Upgrade, flagPluginPath, initArgs.Lockfile, view) + 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) @@ -413,7 +388,7 @@ 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, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { +func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extraConfig arguments.FlagNameValueSlice, 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() @@ -442,7 +417,7 @@ 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, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { +func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, extraConfig arguments.FlagNameValueSlice, 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() @@ -604,10 +579,10 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, view.Output(views.InitializingProviderPluginMessage) }, ProviderAlreadyInstalled: func(provider addrs.Provider, selectedVersion getproviders.Version) { - view.Log(views.ProviderAlreadyInstalledMessage, provider.ForDisplay(), selectedVersion) + view.LogInitMessage(views.ProviderAlreadyInstalledMessage, provider.ForDisplay(), selectedVersion) }, BuiltInProviderAvailable: func(provider addrs.Provider) { - view.Log(views.BuiltInProviderAvailableMessage, provider.ForDisplay()) + view.LogInitMessage(views.BuiltInProviderAvailableMessage, provider.ForDisplay()) }, BuiltInProviderFailure: func(provider addrs.Provider, err error) { diags = diags.Append(tfdiags.Sourceless( @@ -618,20 +593,20 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, }, QueryPackagesBegin: func(provider addrs.Provider, versionConstraints getproviders.VersionConstraints, locked bool) { if locked { - view.Log(views.ReusingPreviousVersionInfo, provider.ForDisplay()) + view.LogInitMessage(views.ReusingPreviousVersionInfo, provider.ForDisplay()) } else { if len(versionConstraints) > 0 { - view.Log(views.FindingMatchingVersionMessage, provider.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints)) + view.LogInitMessage(views.FindingMatchingVersionMessage, provider.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints)) } else { - view.Log(views.FindingLatestVersionMessage, provider.ForDisplay()) + view.LogInitMessage(views.FindingLatestVersionMessage, provider.ForDisplay()) } } }, LinkFromCacheBegin: func(provider addrs.Provider, version getproviders.Version, cacheRoot string) { - view.Log(views.UsingProviderFromCacheDirInfo, provider.ForDisplay(), version) + view.LogInitMessage(views.UsingProviderFromCacheDirInfo, provider.ForDisplay(), version) }, FetchPackageBegin: func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation) { - view.Log(views.InstallingProviderMessage, provider.ForDisplay(), version) + view.LogInitMessage(views.InstallingProviderMessage, provider.ForDisplay(), version) }, QueryPackagesFailure: func(provider addrs.Provider, err error) { switch errorTy := err.(type) { @@ -831,7 +806,7 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, keyID = view.PrepareMessage(views.KeyID, keyID) } - view.Log(views.InstalledProviderVersionInfo, provider.ForDisplay(), version, authResult, keyID) + view.LogInitMessage(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 @@ -877,7 +852,7 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, } } if thirdPartySigned { - view.Log(views.PartnerAndCommunityProvidersMessage) + view.LogInitMessage(views.PartnerAndCommunityProvidersMessage) } }, } @@ -983,7 +958,7 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, // // If the returned diagnostics contains errors then the returned body may be // incomplete or invalid. -func (c *InitCommand) backendConfigOverrideBody(flags rawFlags, schema *configschema.Block) (hcl.Body, tfdiags.Diagnostics) { +func (c *InitCommand) backendConfigOverrideBody(flags arguments.FlagNameValueSlice, schema *configschema.Block) (hcl.Body, tfdiags.Diagnostics) { items := flags.AllItems() if len(items) == 0 { return nil, nil diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 62ad7f284674..feb884979345 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -19,6 +19,7 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/depsfile" @@ -494,7 +495,7 @@ func TestInit_backendConfigFile(t *testing.T) { }, }, } - flagConfigExtra := newRawFlags("-backend-config") + flagConfigExtra := arguments.NewFlagNameValueSlice("-backend-config") flagConfigExtra.Set("input.config") _, diags := c.backendConfigOverrideBody(flagConfigExtra, schema) if len(diags) != 0 { diff --git a/internal/command/meta.go b/internal/command/meta.go index 0d832251697a..c9dd11a71d7d 100644 --- a/internal/command/meta.go +++ b/internal/command/meta.go @@ -204,7 +204,7 @@ type Meta struct { backendState *workdir.BackendState // Variables for the context (private) - variableArgs rawFlags + variableArgs arguments.FlagNameValueSlice input bool // Targets for this context (private) @@ -579,11 +579,11 @@ func (m *Meta) extendedFlagSet(n string) *flag.FlagSet { f := m.defaultFlagSet(n) f.BoolVar(&m.input, "input", true, "input") - f.Var((*FlagStringSlice)(&m.targetFlags), "target", "resource to target") + f.Var((*arguments.FlagStringSlice)(&m.targetFlags), "target", "resource to target") f.BoolVar(&m.compactWarnings, "compact-warnings", false, "use compact warnings") - if m.variableArgs.items == nil { - m.variableArgs = newRawFlags("-var") + if m.variableArgs.Items == nil { + m.variableArgs = arguments.NewFlagNameValueSlice("-var") } varValues := m.variableArgs.Alias("-var") varFiles := m.variableArgs.Alias("-var-file") diff --git a/internal/command/meta_config.go b/internal/command/meta_config.go index 917d9d08a834..594560c1a1d0 100644 --- a/internal/command/meta_config.go +++ b/internal/command/meta_config.go @@ -411,60 +411,3 @@ func configValueFromCLI(synthFilename, rawValue string, wantType cty.Type) (cty. return val, diags } } - -// rawFlags is a flag.Value implementation that just appends raw flag -// names and values to a slice. -type rawFlags struct { - flagName string - items *[]rawFlag -} - -func newRawFlags(flagName string) rawFlags { - var items []rawFlag - return rawFlags{ - flagName: flagName, - items: &items, - } -} - -func (f rawFlags) Empty() bool { - if f.items == nil { - return true - } - return len(*f.items) == 0 -} - -func (f rawFlags) AllItems() []rawFlag { - if f.items == nil { - return nil - } - return *f.items -} - -func (f rawFlags) Alias(flagName string) rawFlags { - return rawFlags{ - flagName: flagName, - items: f.items, - } -} - -func (f rawFlags) String() string { - return "" -} - -func (f rawFlags) Set(str string) error { - *f.items = append(*f.items, rawFlag{ - Name: f.flagName, - Value: str, - }) - return nil -} - -type rawFlag struct { - Name string - Value string -} - -func (f rawFlag) String() string { - return fmt.Sprintf("%s=%q", f.Name, f.Value) -} diff --git a/internal/command/meta_vars.go b/internal/command/meta_vars.go index 18a151678529..dc0690232526 100644 --- a/internal/command/meta_vars.go +++ b/internal/command/meta_vars.go @@ -150,13 +150,13 @@ func (m *Meta) collectVariableValues() (map[string]backendrun.UnparsedVariableVa // Finally we process values given explicitly on the command line, either // as individual literal settings or as additional files to read. - for _, rawFlag := range m.variableArgs.AllItems() { - switch rawFlag.Name { + for _, flagNameValue := range m.variableArgs.AllItems() { + switch flagNameValue.Name { case "-var": // Value should be in the form "name=value", where value is a // raw string whose interpretation will depend on the variable's // parsing mode. - raw := rawFlag.Value + raw := flagNameValue.Value eq := strings.Index(raw, "=") if eq == -1 { diags = diags.Append(tfdiags.Sourceless( @@ -183,13 +183,13 @@ func (m *Meta) collectVariableValues() (map[string]backendrun.UnparsedVariableVa } case "-var-file": - moreDiags := m.addVarsFromFile(rawFlag.Value, terraform.ValueFromNamedFile, ret) + moreDiags := m.addVarsFromFile(flagNameValue.Value, terraform.ValueFromNamedFile, ret) diags = diags.Append(moreDiags) default: // Should never happen; always a bug in the code that built up // the contents of m.variableArgs. - diags = diags.Append(fmt.Errorf("unsupported variable option name %q (this is a bug in Terraform)", rawFlag.Name)) + diags = diags.Append(fmt.Errorf("unsupported variable option name %q (this is a bug in Terraform)", flagNameValue.Name)) } } diff --git a/internal/command/plan.go b/internal/command/plan.go index 056dfd3b9731..a477b4061a90 100644 --- a/internal/command/plan.go +++ b/internal/command/plan.go @@ -200,12 +200,12 @@ func (c *PlanCommand) GatherVariables(opReq *backendrun.Operation, args *argumen // package directly, removing this shim layer. varArgs := args.All() - items := make([]rawFlag, len(varArgs)) + 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 = rawFlags{items: &items} + c.Meta.variableArgs = arguments.FlagNameValueSlice{Items: &items} opReq.Variables, diags = c.collectVariableValues() return diags diff --git a/internal/command/providers_lock.go b/internal/command/providers_lock.go index c78d30f8bee0..2571bbe33a69 100644 --- a/internal/command/providers_lock.go +++ b/internal/command/providers_lock.go @@ -10,6 +10,7 @@ import ( "os" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/getproviders" "github.com/hashicorp/terraform/internal/providercache" @@ -40,7 +41,7 @@ func (c *ProvidersLockCommand) Synopsis() string { func (c *ProvidersLockCommand) Run(args []string) int { args = c.Meta.process(args) cmdFlags := c.Meta.defaultFlagSet("providers lock") - var optPlatforms FlagStringSlice + var optPlatforms arguments.FlagStringSlice var fsMirrorDir string var netMirrorURL string diff --git a/internal/command/providers_mirror.go b/internal/command/providers_mirror.go index 5b2b127f0cba..a7f0d4556905 100644 --- a/internal/command/providers_mirror.go +++ b/internal/command/providers_mirror.go @@ -13,6 +13,7 @@ import ( "github.com/apparentlymart/go-versions/versions" "github.com/hashicorp/go-getter" + "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/getproviders" "github.com/hashicorp/terraform/internal/httpclient" "github.com/hashicorp/terraform/internal/tfdiags" @@ -33,7 +34,7 @@ func (c *ProvidersMirrorCommand) Synopsis() string { func (c *ProvidersMirrorCommand) Run(args []string) int { args = c.Meta.process(args) cmdFlags := c.Meta.defaultFlagSet("providers mirror") - var optPlatforms FlagStringSlice + var optPlatforms arguments.FlagStringSlice cmdFlags.Var(&optPlatforms, "platform", "target platform") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { diff --git a/internal/command/refresh.go b/internal/command/refresh.go index 15160f98d0e6..4c43d6d5808b 100644 --- a/internal/command/refresh.go +++ b/internal/command/refresh.go @@ -182,12 +182,12 @@ func (c *RefreshCommand) GatherVariables(opReq *backendrun.Operation, args *argu // package directly, removing this shim layer. varArgs := args.All() - items := make([]rawFlag, len(varArgs)) + 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 = rawFlags{items: &items} + c.Meta.variableArgs = arguments.FlagNameValueSlice{Items: &items} opReq.Variables, diags = c.collectVariableValues() return diags diff --git a/internal/command/test.go b/internal/command/test.go index cbe2d0b19f8d..bab14fc38b83 100644 --- a/internal/command/test.go +++ b/internal/command/test.go @@ -28,13 +28,13 @@ func (c *TestCommand) Help() string { helpText := ` Usage: terraform [global options] test [options] - Executes automated integration tests against the current Terraform + Executes automated integration tests against the current Terraform configuration. - Terraform will search for .tftest.hcl files within the current configuration - and testing directories. Terraform will then execute the testing run blocks - within any testing files in order, and verify conditional checks and - assertions against the created infrastructure. + Terraform will search for .tftest.hcl files within the current configuration + and testing directories. Terraform will then execute the testing run blocks + within any testing files in order, and verify conditional checks and + assertions against the created infrastructure. This command creates real infrastructure and will attempt to clean up the testing infrastructure on completion. Monitor the output carefully to ensure @@ -42,11 +42,11 @@ Usage: terraform [global options] test [options] Options: - -cloud-run=source If specified, Terraform will execute this test run - remotely using Terraform Cloud. You must specify the + -cloud-run=source If specified, Terraform will execute this test run + remotely using Terraform Cloud. You must specify the source of a module registered in a private module - registry as the argument to this flag. This allows - Terraform to associate the cloud run with the correct + registry as the argument to this flag. This allows + Terraform to associate the cloud run with the correct Terraform Cloud module and organization. -filter=testfile If specified, Terraform will only execute the test files @@ -58,7 +58,7 @@ Options: -no-color If specified, output won't contain any color. - -test-directory=path Set the Terraform test directory, defaults to "tests". + -test-directory=path Set the Terraform test directory, defaults to "tests". -var 'foo=bar' Set a value for one of the input variables in the root module of the configuration. Use this option more than @@ -147,14 +147,14 @@ func (c *TestCommand) Run(rawArgs []string) int { // Users can also specify variables via the command line, so we'll parse // all that here. - var items []rawFlag + var items []arguments.FlagNameValue for _, variable := range args.Vars.All() { - items = append(items, rawFlag{ + items = append(items, arguments.FlagNameValue{ Name: variable.Name, Value: variable.Value, }) } - c.variableArgs = rawFlags{items: &items} + c.variableArgs = arguments.FlagNameValueSlice{Items: &items} // Collect variables for "terraform test" testVariables, variableDiags := c.collectVariableValuesForTests(args.TestDirectory) diff --git a/internal/command/testdata/init-get/output.jsonlog b/internal/command/testdata/init-get/output.jsonlog index 642606d7704b..87f2d2534f73 100644 --- a/internal/command/testdata/init-get/output.jsonlog +++ b/internal/command/testdata/init-get/output.jsonlog @@ -3,6 +3,5 @@ {"@level":"info","@message":"Initializing modules...","@module":"terraform.ui","type":"init_output"} {"@level":"info","@message":"- foo in foo","@module":"terraform.ui","type":"log"} {"@level":"info","@message":"Initializing provider plugins...","@module":"terraform.ui","type":"init_output"} -{"@level":"info","@message":"","@module":"terraform.ui","type":"init_output"} {"@level":"info","@message":"Terraform has been successfully initialized!","@module":"terraform.ui","type":"init_output"} {"@level":"info","@message":"You may now begin working with Terraform. Try running \"terraform plan\" to see\nany changes that are required for your infrastructure. All Terraform commands\nshould now work.\n\nIf you ever set or change modules or backend configuration for Terraform,\nrerun this command to reinitialize your working directory. If you forget, other\ncommands will detect it and remind you to do so if necessary.","@module":"terraform.ui","type":"init_output"} diff --git a/internal/command/views/init.go b/internal/command/views/init.go index 97076b813bcf..81b5b416f3a9 100644 --- a/internal/command/views/init.go +++ b/internal/command/views/init.go @@ -16,9 +16,10 @@ 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 + Output(messageCode InitMessageCode, params ...any) + LogInitMessage(messageCode InitMessageCode, params ...any) + Log(message string, params ...any) + PrepareMessage(messageCode InitMessageCode, params ...any) string } // NewInit returns Init implementation for the given ViewType. @@ -49,19 +50,24 @@ func (v *InitHuman) Diagnostics(diags tfdiags.Diagnostics) { v.view.Diagnostics(diags) } -func (v *InitHuman) Output(messageCode string, params ...any) { +func (v *InitHuman) Output(messageCode InitMessageCode, params ...any) { v.view.streams.Println(v.PrepareMessage(messageCode, params...)) } -func (v *InitHuman) Log(messageCode string, params ...any) { +func (v *InitHuman) LogInitMessage(messageCode InitMessageCode, params ...any) { v.view.streams.Println(v.PrepareMessage(messageCode, params...)) } -func (v *InitHuman) PrepareMessage(messageCode string, params ...any) string { +// this implements log method for use by interfaces that need to log generic string messages, e.g used for logging in hook_module_install.go +func (v *InitHuman) Log(message string, params ...any) { + v.view.streams.Println(strings.TrimSpace(fmt.Sprintf(message, params...))) +} + +func (v *InitHuman) PrepareMessage(messageCode InitMessageCode, 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 string(messageCode) } if message.HumanValue == "" { @@ -84,29 +90,46 @@ func (v *InitJSON) Diagnostics(diags tfdiags.Diagnostics) { v.view.Diagnostics(diags) } -func (v *InitJSON) Output(messageCode string, params ...any) { - current_timestamp := time.Now().UTC().Format(time.RFC3339) +func (v *InitJSON) Output(messageCode InitMessageCode, params ...any) { + // don't add empty messages to json output + preppedMessage := v.PrepareMessage(messageCode, params...) + if preppedMessage == "" { + return + } + current_timestamp := time.Now().UTC().Format(time.RFC3339) json_data := map[string]string{ - "@level": "info", - "@message": v.PrepareMessage(messageCode, params...), - "@module": "terraform.ui", - "@timestamp": current_timestamp, - "type": "init_output"} + "@level": "info", + "@message": preppedMessage, + "@module": "terraform.ui", + "@timestamp": current_timestamp, + "type": "init_output", + "message_code": string(messageCode), + } 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) LogInitMessage(messageCode InitMessageCode, params ...any) { + preppedMessage := v.PrepareMessage(messageCode, params...) + if preppedMessage == "" { + return + } + + v.view.Log(preppedMessage) } -func (v *InitJSON) PrepareMessage(messageCode string, params ...any) string { +// this implements log method for use by services that need to log generic string messages, e.g usage logging in hook_module_install.go +func (v *InitJSON) Log(message string, params ...any) { + v.view.Log(strings.TrimSpace(fmt.Sprintf(message, params...))) +} + +func (v *InitJSON) PrepareMessage(messageCode InitMessageCode, 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 string(messageCode) } return strings.TrimSpace(fmt.Sprintf(message.JSONValue, params...)) @@ -118,7 +141,7 @@ type InitMessage struct { JSONValue string } -var MessageRegistry map[string]InitMessage = map[string]InitMessage{ +var MessageRegistry map[InitMessageCode]InitMessage = map[InitMessageCode]InitMessage{ "copying_configuration_message": { HumanValue: "[reset][bold]Copying configuration[reset] from %q...", JSONValue: "Copying configuration from %q...", @@ -221,32 +244,34 @@ var MessageRegistry map[string]InitMessage = map[string]InitMessage{ }, } +type InitMessageCode string + 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" + CopyingConfigurationMessage InitMessageCode = "copying_configuration_message" + EmptyMessage InitMessageCode = "empty_message" + OutputInitEmptyMessage InitMessageCode = "output_init_empty_message" + OutputInitSuccessMessage InitMessageCode = "output_init_success_message" + OutputInitSuccessCloudMessage InitMessageCode = "output_init_success_cloud_message" + OutputInitSuccessCLIMessage InitMessageCode = "output_init_success_cli_message" + OutputInitSuccessCLICloudMessage InitMessageCode = "output_init_success_cli_cloud_message" + UpgradingModulesMessage InitMessageCode = "upgrading_modules_message" + InitializingTerraformCloudMessage InitMessageCode = "initializing_terraform_cloud_message" + InitializingModulesMessage InitMessageCode = "initializing_modules_message" + InitializingBackendMessage InitMessageCode = "initializing_backend_message" + InitializingProviderPluginMessage InitMessageCode = "initializing_provider_plugin_message" + LockInfo InitMessageCode = "lock_info" + DependenciesLockChangesInfo InitMessageCode = "dependencies_lock_changes_info" + ProviderAlreadyInstalledMessage InitMessageCode = "provider_already_installed_message" + BuiltInProviderAvailableMessage InitMessageCode = "built_in_provider_available_message" + ReusingPreviousVersionInfo InitMessageCode = "reusing_previous_version_info" + FindingMatchingVersionMessage InitMessageCode = "finding_matching_version_message" + FindingLatestVersionMessage InitMessageCode = "finding_latest_version_message" + UsingProviderFromCacheDirInfo InitMessageCode = "using_provider_from_cache_dir_info" + InstallingProviderMessage InitMessageCode = "installing_provider_message" + KeyID InitMessageCode = "key_id" + InstalledProviderVersionInfo InitMessageCode = "installed_provider_version_info" + PartnerAndCommunityProvidersMessage InitMessageCode = "partner_and_community_providers_message" + InitConfigError InitMessageCode = "init_config_error" ) const outputInitEmpty = ` diff --git a/internal/command/views/init_test.go b/internal/command/views/init_test.go index fb84d6fad8c6..ae7e84c2caea 100644 --- a/internal/command/views/init_test.go +++ b/internal/command/views/init_test.go @@ -129,8 +129,7 @@ func TestNewInit_jsonViewOutput(t *testing.T) { t.Fatalf("unexpected return type %t", newInit) } - messageCode := "initializing_provider_plugin_message" - newInit.Output(messageCode) + newInit.Output(InitializingProviderPluginMessage) version := tfversion.String() want := []map[string]interface{}{ @@ -163,8 +162,7 @@ func TestNewInit_jsonViewOutput(t *testing.T) { } packageName := "hashicorp/aws" - messageCode := "finding_latest_version_message" - newInit.Output(messageCode, packageName) + newInit.Output(FindingLatestVersionMessage, packageName) version := tfversion.String() want := []map[string]interface{}{ @@ -197,8 +195,7 @@ func TestNewInit_jsonViewOutput(t *testing.T) { } var packageName, packageVersion = "hashicorp/aws", "3.0.0" - messageCode := "provider_already_installed_message" - newInit.Output(messageCode, packageName, packageVersion) + newInit.Output(ProviderAlreadyInstalledMessage, packageName, packageVersion) version := tfversion.String() want := []map[string]interface{}{ @@ -231,8 +228,7 @@ func TestNewInit_jsonViewLog(t *testing.T) { t.Fatalf("unexpected return type %t", newInit) } - messageCode := "initializing_provider_plugin_message" - newInit.Log(messageCode) + newInit.LogInitMessage(InitializingProviderPluginMessage) version := tfversion.String() want := []map[string]interface{}{ @@ -257,23 +253,6 @@ func TestNewInit_jsonViewLog(t *testing.T) { } func TestNewInit_jsonViewPrepareMessage(t *testing.T) { - t.Run("message code that does not exists", func(t *testing.T) { - streams, _ := terminal.StreamsForTesting(t) - - newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) - if _, ok := newInit.(*InitJSON); !ok { - t.Fatalf("unexpected return type %t", newInit) - } - - messageCode := "Terraform has been successfully initialized!" - want := messageCode - - actual := newInit.PrepareMessage(messageCode) - if !cmp.Equal(want, actual) { - t.Errorf("unexpected output: %s", cmp.Diff(want, actual)) - } - }) - t.Run("existing message code", func(t *testing.T) { streams, _ := terminal.StreamsForTesting(t) @@ -282,10 +261,9 @@ func TestNewInit_jsonViewPrepareMessage(t *testing.T) { t.Fatalf("unexpected return type %t", newInit) } - messageCode := "initializing_modules_message" want := "Initializing modules..." - actual := newInit.PrepareMessage(messageCode) + actual := newInit.PrepareMessage(InitializingModulesMessage) if !cmp.Equal(want, actual) { t.Errorf("unexpected output: %s", cmp.Diff(want, actual)) } @@ -301,8 +279,7 @@ func TestNewInit_humanViewOutput(t *testing.T) { t.Fatalf("unexpected return type %t", newInit) } - messageCode := "initializing_provider_plugin_message" - newInit.Output(messageCode) + newInit.Output(InitializingProviderPluginMessage) actual := done(t).All() expected := "Initializing provider plugins..." @@ -320,8 +297,7 @@ func TestNewInit_humanViewOutput(t *testing.T) { } packageName := "hashicorp/aws" - messageCode := "finding_latest_version_message" - newInit.Output(messageCode, packageName) + newInit.Output(FindingLatestVersionMessage, packageName) actual := done(t).All() expected := "Finding latest version of hashicorp/aws" @@ -339,8 +315,7 @@ func TestNewInit_humanViewOutput(t *testing.T) { } var packageName, packageVersion = "hashicorp/aws", "3.0.0" - messageCode := "provider_already_installed_message" - newInit.Output(messageCode, packageName, packageVersion) + newInit.Output(ProviderAlreadyInstalledMessage, packageName, packageVersion) actual := done(t).All() expected := "- Using previously-installed hashicorp/aws v3.0.0" From 7fb13b886819d0dd948eb169b0628405424e310e Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Tue, 16 Apr 2024 02:09:09 -0400 Subject: [PATCH 11/13] fixing tests --- internal/command/arguments/init_test.go | 114 ++++++++++++++++-- .../command/testdata/init-get/output.jsonlog | 10 +- internal/command/views/init_test.go | 27 +++-- 3 files changed, 124 insertions(+), 27 deletions(-) diff --git a/internal/command/arguments/init_test.go b/internal/command/arguments/init_test.go index 97f7c76a34c2..93e13b7b6281 100644 --- a/internal/command/arguments/init_test.go +++ b/internal/command/arguments/init_test.go @@ -9,9 +9,11 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" ) func TestParseInit_basicValid(t *testing.T) { + var flagNameValue []FlagNameValue testCases := map[string]struct { args []string want *Init @@ -34,13 +36,21 @@ func TestParseInit_basicValid(t *testing.T) { Upgrade: false, Json: false, IgnoreRemoteVersion: false, + BackendConfig: FlagNameValueSlice{ + FlagName: "-backend-config", + Items: &flagNameValue, + }, + Vars: &Vars{}, + InputEnabled: true, + CompactWarnings: false, + TargetFlags: nil, }, }, "setting multiple options": { []string{"-backend=false", "-force-copy=true", "-from-module=./main-dir", "-json", "-get=false", "-lock=false", "-lock-timeout=10s", "-reconfigure=true", - "-upgrade=true", "-lockfile=readonly", + "-upgrade=true", "-lockfile=readonly", "-compact-warnings=true", "-ignore-remote-version=true", "-test-directory=./test-dir"}, &Init{ FromModule: "./main-dir", @@ -58,10 +68,19 @@ func TestParseInit_basicValid(t *testing.T) { Upgrade: true, Json: true, IgnoreRemoteVersion: true, + BackendConfig: FlagNameValueSlice{ + FlagName: "-backend-config", + Items: &flagNameValue, + }, + Vars: &Vars{}, + InputEnabled: true, + Args: []string{}, + CompactWarnings: true, + TargetFlags: nil, }, }, "with cloud option": { - []string{"-cloud=false"}, + []string{"-cloud=false", "-input=false", "-target=foo_bar.baz", "-backend-config", "backend.config"}, &Init{ FromModule: "", Lockfile: "", @@ -78,10 +97,21 @@ func TestParseInit_basicValid(t *testing.T) { Upgrade: false, Json: false, IgnoreRemoteVersion: false, + BackendConfig: FlagNameValueSlice{ + FlagName: "-backend-config", + Items: &[]FlagNameValue{{Name: "-backend-config", Value: "backend.config"}}, + }, + Vars: &Vars{}, + InputEnabled: false, + Args: []string{}, + CompactWarnings: false, + TargetFlags: []string{"foo_bar.baz"}, }, }, } + cmpOpts := cmpopts.IgnoreUnexported(Vars{}) + for name, tc := range testCases { t.Run(name, func(t *testing.T) { got, diags := ParseInit(tc.args) @@ -89,7 +119,7 @@ func TestParseInit_basicValid(t *testing.T) { t.Fatalf("unexpected diags: %v", diags) } - if diff := cmp.Diff(tc.want, got); diff != "" { + if diff := cmp.Diff(tc.want, got, cmpOpts); diff != "" { t.Errorf("unexpected result\n%s", diff) } }) @@ -98,16 +128,29 @@ func TestParseInit_basicValid(t *testing.T) { func TestParseInit_invalid(t *testing.T) { testCases := map[string]struct { - args []string - wantErr string + args []string + wantErr string + wantViewType ViewType }{ "with unsupported options": { - args: []string{"-raw"}, - wantErr: "flag provided but not defined", + args: []string{"-raw"}, + wantErr: "flag provided but not defined", + wantViewType: ViewHuman, }, "with both -backend and -cloud options set": { - args: []string{"-backend=false", "-cloud=false"}, - wantErr: "The -backend and -cloud options are aliases of one another and mutually-exclusive in their use", + args: []string{"-backend=false", "-cloud=false"}, + wantErr: "The -backend and -cloud options are aliases of one another and mutually-exclusive in their use", + wantViewType: ViewHuman, + }, + "with both -migrate-state and -json options set": { + args: []string{"-migrate-state", "-json"}, + wantErr: "Terraform cannot ask for interactive approval when -json is set. To use the -migrate-state option, disable the -json option.", + wantViewType: ViewJSON, + }, + "with both -migrate-state and -reconfigure options set": { + args: []string{"-migrate-state", "-reconfigure"}, + wantErr: "The -migrate-state and -reconfigure options are mutually-exclusive.", + wantViewType: ViewHuman, }, } @@ -120,9 +163,60 @@ func TestParseInit_invalid(t *testing.T) { if got, want := diags.Err().Error(), tc.wantErr; !strings.Contains(got, want) { t.Fatalf("wrong diags\n got: %s\nwant: %s", got, want) } - if got.ViewType != ViewHuman { + if got.ViewType != tc.wantViewType { t.Fatalf("wrong view type, got %#v, want %#v", got.ViewType, ViewHuman) } }) } } + +func TestParseInit_vars(t *testing.T) { + testCases := map[string]struct { + args []string + want []FlagNameValue + }{ + "no var flags by default": { + args: nil, + want: nil, + }, + "one var": { + args: []string{"-var", "foo=bar"}, + want: []FlagNameValue{ + {Name: "-var", Value: "foo=bar"}, + }, + }, + "one var-file": { + args: []string{"-var-file", "cool.tfvars"}, + want: []FlagNameValue{ + {Name: "-var-file", Value: "cool.tfvars"}, + }, + }, + "ordering preserved": { + args: []string{ + "-var", "foo=bar", + "-var-file", "cool.tfvars", + "-var", "boop=beep", + }, + want: []FlagNameValue{ + {Name: "-var", Value: "foo=bar"}, + {Name: "-var-file", Value: "cool.tfvars"}, + {Name: "-var", Value: "boop=beep"}, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseInit(tc.args) + if len(diags) > 0 { + t.Fatalf("unexpected diags: %v", diags) + } + if vars := got.Vars.All(); !cmp.Equal(vars, tc.want) { + t.Fatalf("unexpected result\n%s", cmp.Diff(vars, tc.want)) + } + if got, want := got.Vars.Empty(), len(tc.want) == 0; got != want { + t.Fatalf("expected Empty() to return %t, but was %t", want, got) + } + }) + } +} diff --git a/internal/command/testdata/init-get/output.jsonlog b/internal/command/testdata/init-get/output.jsonlog index 87f2d2534f73..88acf532fd07 100644 --- a/internal/command/testdata/init-get/output.jsonlog +++ b/internal/command/testdata/init-get/output.jsonlog @@ -1,7 +1,7 @@ {"@level":"info","@message":"Terraform 1.9.0-dev","@module":"terraform.ui","terraform":"1.9.0-dev","type":"version","ui":"1.2"} -{"@level":"info","@message":"Initializing the backend...","@module":"terraform.ui","type":"init_output"} -{"@level":"info","@message":"Initializing modules...","@module":"terraform.ui","type":"init_output"} +{"@level":"info","@message":"Initializing the backend...","@module":"terraform.ui","message_code": "initializing_backend_message","type":"init_output"} +{"@level":"info","@message":"Initializing modules...","@module":"terraform.ui","message_code": "initializing_modules_message","type":"init_output"} {"@level":"info","@message":"- foo in foo","@module":"terraform.ui","type":"log"} -{"@level":"info","@message":"Initializing provider plugins...","@module":"terraform.ui","type":"init_output"} -{"@level":"info","@message":"Terraform has been successfully initialized!","@module":"terraform.ui","type":"init_output"} -{"@level":"info","@message":"You may now begin working with Terraform. Try running \"terraform plan\" to see\nany changes that are required for your infrastructure. All Terraform commands\nshould now work.\n\nIf you ever set or change modules or backend configuration for Terraform,\nrerun this command to reinitialize your working directory. If you forget, other\ncommands will detect it and remind you to do so if necessary.","@module":"terraform.ui","type":"init_output"} +{"@level":"info","@message":"Initializing provider plugins...","@module":"terraform.ui","message_code": "initializing_provider_plugin_message","type":"init_output"} +{"@level":"info","@message":"Terraform has been successfully initialized!","@module":"terraform.ui","message_code": "output_init_success_message","type":"init_output"} +{"@level":"info","@message":"You may now begin working with Terraform. Try running \"terraform plan\" to see\nany changes that are required for your infrastructure. All Terraform commands\nshould now work.\n\nIf you ever set or change modules or backend configuration for Terraform,\nrerun this command to reinitialize your working directory. If you forget, other\ncommands will detect it and remind you to do so if necessary.","@module":"terraform.ui","message_code": "output_init_success_cli_message","type":"init_output"} diff --git a/internal/command/views/init_test.go b/internal/command/views/init_test.go index ae7e84c2caea..2f056cb0c9e2 100644 --- a/internal/command/views/init_test.go +++ b/internal/command/views/init_test.go @@ -142,10 +142,11 @@ func TestNewInit_jsonViewOutput(t *testing.T) { "ui": JSON_UI_VERSION, }, { - "@level": "info", - "@message": "Initializing provider plugins...", - "@module": "terraform.ui", - "type": "init_output", + "@level": "info", + "@message": "Initializing provider plugins...", + "message_code": "initializing_provider_plugin_message", + "@module": "terraform.ui", + "type": "init_output", }, } @@ -175,10 +176,11 @@ func TestNewInit_jsonViewOutput(t *testing.T) { "ui": JSON_UI_VERSION, }, { - "@level": "info", - "@message": fmt.Sprintf("- Finding latest version of %s...", packageName), - "@module": "terraform.ui", - "type": "init_output", + "@level": "info", + "@message": fmt.Sprintf("- Finding latest version of %s...", packageName), + "@module": "terraform.ui", + "message_code": "finding_latest_version_message", + "type": "init_output", }, } @@ -208,10 +210,11 @@ func TestNewInit_jsonViewOutput(t *testing.T) { "ui": JSON_UI_VERSION, }, { - "@level": "info", - "@message": fmt.Sprintf("- Using previously-installed %s v%s", packageName, packageVersion), - "@module": "terraform.ui", - "type": "init_output", + "@level": "info", + "@message": fmt.Sprintf("- Using previously-installed %s v%s", packageName, packageVersion), + "@module": "terraform.ui", + "message_code": "provider_already_installed_message", + "type": "init_output", }, } From a4415c030afc6444dfef2ac12958ea38b16fffa1 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Wed, 17 Apr 2024 09:25:52 -0400 Subject: [PATCH 12/13] making json data more useful for client consumption --- internal/command/views/init.go | 18 +++++++++--------- internal/command/views/init_test.go | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/command/views/init.go b/internal/command/views/init.go index 81b5b416f3a9..15ff7f80126f 100644 --- a/internal/command/views/init.go +++ b/internal/command/views/init.go @@ -196,39 +196,39 @@ var MessageRegistry map[InitMessageCode]InitMessage = map[InitMessageCode]InitMe }, "provider_already_installed_message": { HumanValue: "- Using previously-installed %s v%s", - JSONValue: "- Using previously-installed %s v%s", + JSONValue: "%s v%s: Using previously-installed provider version", }, "built_in_provider_available_message": { HumanValue: "- %s is built in to Terraform", - JSONValue: "- %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", + JSONValue: "%s: Reusing previous version from the dependency lock file", }, "finding_matching_version_message": { HumanValue: "- Finding %s versions matching %q...", - JSONValue: "- Finding %s versions matching %q...", + JSONValue: "Finding matching versions for provider: %s, version_constraint: %q", }, "finding_latest_version_message": { HumanValue: "- Finding latest version of %s...", - JSONValue: "- Finding latest version of %s...", + JSONValue: "%s: Finding latest version...", }, "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", + JSONValue: "%s v%s: Using from the shared cache directory", }, "installing_provider_message": { HumanValue: "- Installing %s v%s...", - JSONValue: "- Installing %s v%s...", + JSONValue: "Installing provider version: %s v%s...", }, "key_id": { HumanValue: ", key ID [reset][bold]%s[reset]", - JSONValue: ", key ID %s", + JSONValue: "key_id: %s", }, "installed_provider_version_info": { HumanValue: "- Installed %s v%s (%s%s)", - JSONValue: "- Installed %s v%s (%s%s)", + JSONValue: "Installed provider version: %s v%s (%s%s)", }, "partner_and_community_providers_message": { HumanValue: partnerAndCommunityProvidersInfo, diff --git a/internal/command/views/init_test.go b/internal/command/views/init_test.go index 2f056cb0c9e2..5017d714772b 100644 --- a/internal/command/views/init_test.go +++ b/internal/command/views/init_test.go @@ -177,7 +177,7 @@ func TestNewInit_jsonViewOutput(t *testing.T) { }, { "@level": "info", - "@message": fmt.Sprintf("- Finding latest version of %s...", packageName), + "@message": fmt.Sprintf("%s: Finding latest version...", packageName), "@module": "terraform.ui", "message_code": "finding_latest_version_message", "type": "init_output", @@ -211,7 +211,7 @@ func TestNewInit_jsonViewOutput(t *testing.T) { }, { "@level": "info", - "@message": fmt.Sprintf("- Using previously-installed %s v%s", packageName, packageVersion), + "@message": fmt.Sprintf("%s v%s: Using previously-installed provider version", packageName, packageVersion), "@module": "terraform.ui", "message_code": "provider_already_installed_message", "type": "init_output", From f1fb26e480daedd21c4dabdc962f15bc6843b1b6 Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Wed, 17 Apr 2024 13:25:59 -0400 Subject: [PATCH 13/13] update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d716cdfdabe2..97b369560111 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ ENHANCEMENTS: If an entered line contains opening paretheses/etc that are not closed, Terraform will await another line of input to complete the expression. This initial implementation is primarily intended to support pasting in multi-line expressions from elsewhere, rather than for manual multi-line editing, so the interactive editing support is currently limited. * `cli`: Updates the Terraform CLI output to show logical separation between OPA and Sentinel policy evaluations +* `terraform init` now accepts a `-json` option. If specified, enables the machine readable JSON output. ([#34886](https://github.com/hashicorp/terraform/pull/34886)) BUG FIXES: @@ -19,7 +20,7 @@ Experiments are only enabled in alpha releases of Terraform CLI. The following f * `variable_validation_crossref`: This [language experiment](https://developer.hashicorp.com/terraform/language/settings#experimental-language-features) allows `validation` blocks inside input variable declarations to refer to other objects inside the module where the variable is declared, including to the values of other input variables in the same module. * `terraform test` accepts a new option `-junit-xml=FILENAME`. If specified, and if the test configuration is valid enough to begin executing, then Terraform writes a JUnit XML test result report to the given filename, describing similar information as included in the normal test output. ([#34291](https://github.com/hashicorp/terraform/issues/34291)) * The new command `terraform rpcapi` exposes some Terraform Core functionality through an RPC interface compatible with [`go-plugin`](https://github.com/hashicorp/go-plugin). The exact RPC API exposed here is currently subject to change at any time, because it's here primarily as a vehicle to support the [Terraform Stacks](https://www.hashicorp.com/blog/terraform-stacks-explained) private preview and so will be broken if necessary to respond to feedback from private preview participants, or possibly for other reasons. Do not use this mechanism yet outside of Terraform Stacks private preview. -* The experimental "deferred actions" feature, enabled by passing the `-allow-deferral` option to `terraform plan`, permits `count` and `for_each` arguments in `module`, `resource`, and `data` blocks to have unknown values and allows providers to react more flexibly to unknown values. This experiment is under active development, and so it's not yet useful to participate in this experiment. +* The experimental "deferred actions" feature, enabled by passing the `-allow-deferral` option to `terraform plan`, permits `count` and `for_each` arguments in `module`, `resource`, and `data` blocks to have unknown values and allows providers to react more flexibly to unknown values. This experiment is under active development, and so it's not yet useful to participate in this experiment. ## Previous Releases