Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/command/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
15 changes: 7 additions & 8 deletions internal/command/init_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
95 changes: 95 additions & 0 deletions internal/command/views/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
147 changes: 147 additions & 0 deletions internal/command/views/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down