diff --git a/internal/command/init.go b/internal/command/init.go index c56db136cc5f..06d8bec59bea 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -146,7 +146,7 @@ func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extra _ = ctx // prevent staticcheck from complaining to avoid a maintenance hazard of having the wrong ctx in scope here defer span.End() - view.Output(views.InitializingTerraformCloudMessage) + view.LogCloudInitializationStart() if len(extraConfig.AllItems()) != 0 { diags = diags.Append(tfdiags.Sourceless( diff --git a/internal/command/init_run.go b/internal/command/init_run.go index cae8c7567297..bd9474f7ea19 100644 --- a/internal/command/init_run.go +++ b/internal/command/init_run.go @@ -110,7 +110,7 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int { return 1 } if empty { - view.Output(views.OutputInitEmptyMessage) + view.LogInitializationComplete(true) return 0 } @@ -446,22 +446,21 @@ Please use \"terraform state migrate -upgrade\" to upgrade the state store provi // still the final thing shown. view.Diagnostics(diags) _, cloud := back.(*cloud.Cloud) - output := views.OutputInitSuccessMessage if cloud { - output = views.OutputInitSuccessCloudMessage + view.LogCloudInitializationComplete() + } else { + view.LogInitializationComplete(false) } - view.Output(output) - if !c.RunningInAutomation { // If we're not running in an automation wrapper, give the user // some more detailed next steps that are appropriate for interactive // shell usage. - output = views.OutputInitSuccessCLIMessage if cloud { - output = views.OutputInitSuccessCLICloudMessage + view.LogCloudInitializationCompleteCallToAction() + } else { + view.LogInitializationCompleteCallToAction() } - view.Output(output) } return 0 } diff --git a/internal/command/views/init.go b/internal/command/views/init.go index cf8d75f1b1f0..54ce725e91fe 100644 --- a/internal/command/views/init.go +++ b/internal/command/views/init.go @@ -22,6 +22,24 @@ type Init interface { PolicyDiagnostics(diags policy.Diagnostics) Output(messageCode InitMessageCode, params ...any) + // LogCloudInitializationStart describes the start of initializing the cloud backend + LogCloudInitializationStart() + // LogCloudInitializationComplete describes the successful end of initializing the workspace + // while using the cloud backend + LogCloudInitializationComplete() + // LogCloudInitializationCompleteCallToAction prompts users about what to do next after initialization. + // This is only used if the cloud backend is used, and the CLI is being used outside of automation; a human will see the CTA. + LogCloudInitializationCompleteCallToAction() + + // LogInitializationComplete describes the successful end of initializing the workspace. + // + // Output is different depending on whether the configuration is empty or contains resources. Calling code should report if + // the config is empty via the boolean. + LogInitializationComplete(emptyDirectory bool) + // LogInitializationCompleteCallToAction prompts users about what to do next after initialization. + // This is only used if the CLI is being used outside of automation; a human will see the CTA. + LogInitializationCompleteCallToAction() + ModuleInstallationLogger ProviderInstallationLogger DependencyLockingLogger @@ -78,6 +96,37 @@ func (v *InitHuman) Output(messageCode InitMessageCode, params ...any) { v.print(v.prepareMessage(messageCode, params...)) } +func (v *InitHuman) LogCloudInitializationStart() { + params := []any{} + v.print(v.prepareMessage(InitializingTerraformCloudMessage, params...)) +} + +func (v *InitHuman) LogCloudInitializationComplete() { + params := []any{} + v.print(v.prepareMessage(OutputInitSuccessCloudMessage, params...)) +} + +func (v *InitHuman) LogCloudInitializationCompleteCallToAction() { + params := []any{} + v.print(v.prepareMessage(OutputInitSuccessCLICloudMessage, params...)) +} + +func (v *InitHuman) LogInitializationCompleteCallToAction() { + params := []any{} + v.print(v.prepareMessage(OutputInitSuccessCLIMessage, params...)) +} + +func (v *InitHuman) LogInitializationComplete(emptyDirectory bool) { + params := []any{} + var code InitMessageCode + if emptyDirectory { + code = OutputInitEmptyMessage + } else { + code = OutputInitSuccessMessage + } + v.print(v.prepareMessage(code, params...)) +} + func (v *InitHuman) LogInitializingStateStoreProviderPlugin(pAddr tfaddr.Provider, cons getproviders.VersionConstraints, storeType string) { consSuffix := "" if len(cons) > 0 { @@ -251,6 +300,52 @@ func (v *InitJSON) Output(messageCode InitMessageCode, params ...any) { ) } +func (v *InitJSON) LogCloudInitializationStart() { + params := []any{} + + // This was previously logged via Output, so we need to match implementation of that method + // to ensure the same JSON log is produced. + v.Output(InitializingTerraformCloudMessage, params...) +} + +func (v *InitJSON) LogCloudInitializationComplete() { + params := []any{} + + // This was previously logged via Output, so we need to match implementation of that method + // to ensure the same JSON log is produced. + v.Output(OutputInitSuccessCloudMessage, params...) +} + +func (v *InitJSON) LogCloudInitializationCompleteCallToAction() { + params := []any{} + + // This was previously logged via Output, so we need to match implementation of that method + // to ensure the same JSON log is produced. + v.Output(OutputInitSuccessCLICloudMessage, params...) +} + +func (v *InitJSON) LogInitializationCompleteCallToAction() { + params := []any{} + + // This was previously logged via Output, so we need to match implementation of that method + // to ensure the same JSON log is produced. + v.Output(OutputInitSuccessCLIMessage, params...) +} + +func (v *InitJSON) LogInitializationComplete(emptyDirectory bool) { + params := []any{} + var code InitMessageCode + if emptyDirectory { + code = OutputInitEmptyMessage + } else { + code = OutputInitSuccessMessage + } + + // This was previously logged via Output, so we need to match implementation of that method + // to ensure the same JSON log is produced. + v.Output(code, params...) +} + // logInitMessage is an internalised version of an old method `LogInitMessage`. // New methods have since been added that replace the old `LogInitMessage` method, // but to ensure that the same JSON output is produced we keep `logInitMessage` to diff --git a/internal/command/views/init_test.go b/internal/command/views/init_test.go index 494b1306e4ea..2a21990a032b 100644 --- a/internal/command/views/init_test.go +++ b/internal/command/views/init_test.go @@ -1051,6 +1051,153 @@ func TestNewInit_LogModuleInitialization_json(t *testing.T) { } } +func TestNewInit_LogCloudInitializationStart_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + initView := NewInit(arguments.ViewJSON, view) + + initView.LogCloudInitializationStart() + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"Initializing HCP Terraform..."`, + `"@module":"terraform.ui"`, + //@timestamp is dynamic + `"message_code":"initializing_terraform_cloud_message"`, + `"type":"init_output"`, + } + for _, snippet := range expectedOutputFields { + if !strings.Contains(output.Stdout(), snippet) { + t.Fatalf("output didn't include expected snippet:\n expected: %s\n got:\n %s", snippet, output.Stdout()) + } + } +} + +func TestNewInit_LogCloudInitializationComplete_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + initView := NewInit(arguments.ViewJSON, view) + + initView.LogCloudInitializationComplete() + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"HCP Terraform has been successfully initialized!"`, + `"@module":"terraform.ui"`, + //@timestamp is dynamic + `"message_code":"output_init_success_cloud_message"`, + `"type":"init_output"`, + } + for _, snippet := range expectedOutputFields { + if !strings.Contains(output.Stdout(), snippet) { + t.Fatalf("output didn't include expected snippet:\n expected: %s\n got:\n %s", snippet, output.Stdout()) + } + } +} + +func TestNewInit_LogInitializationComplete_json(t *testing.T) { + t.Run("empty config", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + initView := NewInit(arguments.ViewJSON, view) + + empty := true + initView.LogInitializationComplete(empty) + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"Terraform initialized in an empty directory!`, // ... incomplete but sufficient for test + `"@module":"terraform.ui"`, + //@timestamp is dynamic + `"message_code":"output_init_empty_message"`, + `"type":"init_output"`, + } + for _, snippet := range expectedOutputFields { + if !strings.Contains(output.Stdout(), snippet) { + t.Fatalf("output didn't include expected snippet:\n expected: %s\n got:\n %s", snippet, output.Stdout()) + } + } + }) + t.Run("non-empty config", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + initView := NewInit(arguments.ViewJSON, view) + + empty := false + initView.LogInitializationComplete(empty) + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"Terraform has been successfully initialized!"`, + `"@module":"terraform.ui"`, + //@timestamp is dynamic + `"message_code":"output_init_success_message"`, + `"type":"init_output"`, + } + for _, snippet := range expectedOutputFields { + if !strings.Contains(output.Stdout(), snippet) { + t.Fatalf("output didn't include expected snippet:\n expected: %s\n got:\n %s", snippet, output.Stdout()) + } + } + }) +} + +func TestNewInit_LogCloudInitializationCompleteCallToAction_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + initView := NewInit(arguments.ViewJSON, view) + + initView.LogCloudInitializationCompleteCallToAction() + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"You may now begin working with HCP Terraform. Try running \"terraform plan\"`, // ... incomplete but sufficient for test + `"@module":"terraform.ui"`, + //@timestamp is dynamic + `"message_code":"output_init_success_cli_cloud_message"`, + `"type":"init_output"`, + } + for _, snippet := range expectedOutputFields { + if !strings.Contains(output.Stdout(), snippet) { + t.Fatalf("output didn't include expected snippet:\n expected: %s\n got:\n %s", snippet, output.Stdout()) + } + } +} + +func TestNewInit_LogInitializationCompleteCallToAction_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + initView := NewInit(arguments.ViewJSON, view) + + initView.LogInitializationCompleteCallToAction() + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"You may now begin working with Terraform. Try running \"terraform plan\" `, // ... incomplete but sufficient for test + `"@module":"terraform.ui"`, + //@timestamp is dynamic + `"message_code":"output_init_success_cli_message"`, + `"type":"init_output"`, + } + for _, snippet := range expectedOutputFields { + if !strings.Contains(output.Stdout(), snippet) { + t.Fatalf("output didn't include expected snippet:\n expected: %s\n got:\n %s", snippet, output.Stdout()) + } + } +} + func TestNewInit_Spacer_json(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewView(streams)