From 8ff1a753da4f40afa3da473eacbc9466ef5e23ab Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 5 Aug 2026 11:58:31 +0100 Subject: [PATCH 01/14] refactor: Add `print` method on the human init view implementation for formatting and printing messages to stdout --- internal/command/views/init.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/internal/command/views/init.go b/internal/command/views/init.go index 5719093463ea..c17947a2f0b2 100644 --- a/internal/command/views/init.go +++ b/internal/command/views/init.go @@ -163,6 +163,17 @@ func (v *InitHuman) LogModuleInstallation(message string) { v.view.streams.Println(strings.TrimSpace(message)) } +// print formats (trims whitespace & applies colour) and +// prints the formatted message to the stdout stream. +func (v *InitHuman) print(message string) { + message = v.view.colorize.Color(strings.TrimSpace(message)) + v.view.streams.Println(message) +} + +// prepareMessage retrieves a message template matching the InitMessageCode and +// returns a formatted string made using the template and param argument(s). +// +// As this is implemented on InitHuman the human message template is used. func (v *InitHuman) prepareMessage(messageCode InitMessageCode, params ...any) string { message, ok := MessageRegistry[messageCode] if !ok { @@ -174,7 +185,7 @@ func (v *InitHuman) prepareMessage(messageCode InitMessageCode, params ...any) s panic("unexpected empty message for init message code: " + string(messageCode)) } - return v.view.colorize.Color(strings.TrimSpace(fmt.Sprintf(message.HumanValue, params...))) + return fmt.Sprintf(message.HumanValue, params...) } // The InitJSON implementation renders streaming JSON logs, suitable for @@ -365,6 +376,10 @@ func (v *InitJSON) LogModuleInstallation(message string) { v.view.Log(message) } +// prepareMessage retrieves a message template matching the InitMessageCode and +// returns a formatted string made using the template and param argument(s). +// +// As this is implemented on InitJSON the JSON message template is used. func (v *InitJSON) prepareMessage(messageCode InitMessageCode, params ...any) string { message, ok := MessageRegistry[messageCode] if !ok { From 9055c125ed8b593b63ac8ed1ce9ecdd828abe194 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 5 Aug 2026 12:02:13 +0100 Subject: [PATCH 02/14] refactor: Make all `InitHuman` methods print output to stdout via the `print` method. This ensures that all methods perform the same actions on a message string: * Trim whitespace * Apply colour Previously `prepareMessage` did this, which meant that methods that don't use `prepareMessage` were at risk of not formatting messages in the expected way. --- internal/command/views/init.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/internal/command/views/init.go b/internal/command/views/init.go index c17947a2f0b2..fb1ff12210d3 100644 --- a/internal/command/views/init.go +++ b/internal/command/views/init.go @@ -75,7 +75,7 @@ func (v *InitHuman) PolicyResult(addr string, resp policy.EvaluationResponse) { } func (v *InitHuman) Output(messageCode InitMessageCode, params ...any) { - v.view.streams.Println(v.prepareMessage(messageCode, params...)) + v.print(v.prepareMessage(messageCode, params...)) } func (v *InitHuman) LogInitializingStateStoreProviderPlugin(pAddr tfaddr.Provider, cons getproviders.VersionConstraints, storeType string) { @@ -84,83 +84,83 @@ func (v *InitHuman) LogInitializingStateStoreProviderPlugin(pAddr tfaddr.Provide consSuffix = fmt.Sprintf(" (%s)", getproviders.VersionConstraintsString(cons)) } params := []any{pAddr.ForDisplay(), consSuffix, storeType} - v.view.streams.Println(v.prepareMessage(InitializingStateStoreProviderPluginMessage, params...)) + v.print(v.prepareMessage(InitializingStateStoreProviderPluginMessage, params...)) } func (v *InitHuman) LogFindingMatchingVersion(providerAddr addrs.Provider, versionConstraints getproviders.VersionConstraints) { params := []any{providerAddr.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints)} - v.view.streams.Println(v.prepareMessage(FindingMatchingVersionMessage, params...)) + v.print(v.prepareMessage(FindingMatchingVersionMessage, params...)) } func (v *InitHuman) LogFindingLatestVersion(providerAddr addrs.Provider) { params := []any{providerAddr.ForDisplay()} - v.view.streams.Println(v.prepareMessage(FindingLatestVersionMessage, params...)) + v.print(v.prepareMessage(FindingLatestVersionMessage, params...)) } func (v *InitHuman) LogProviderVersionAlreadyInstalled(providerAddr addrs.Provider, version getproviders.Version) { params := []any{providerAddr.ForDisplay(), version} - v.view.streams.Println(v.prepareMessage(ProviderAlreadyInstalledMessage, params...)) + v.print(v.prepareMessage(ProviderAlreadyInstalledMessage, params...)) } func (v *InitHuman) LogUsingProviderVersionFromCacheDir(providerAddr addrs.Provider, version getproviders.Version) { params := []any{providerAddr.ForDisplay(), version} - v.view.streams.Println(v.prepareMessage(UsingProviderFromCacheDirInfo, params...)) + v.print(v.prepareMessage(UsingProviderFromCacheDirInfo, params...)) } func (v *InitHuman) LogBuiltInProviderAvailable(providerAddr addrs.Provider) { params := []any{providerAddr.ForDisplay()} - v.view.streams.Println(v.prepareMessage(BuiltInProviderAvailableMessage, params...)) + v.print(v.prepareMessage(BuiltInProviderAvailableMessage, params...)) } func (v *InitHuman) LogInstallingProviderVersion(providerAddr addrs.Provider, version getproviders.Version) { params := []any{providerAddr.ForDisplay(), version} - v.view.streams.Println(v.prepareMessage(InstallingProviderMessage, params...)) + v.print(v.prepareMessage(InstallingProviderMessage, params...)) } func (v *InitHuman) LogReusingPreviousProviderVersion(providerAddr addrs.Provider, version getproviders.Version) { params := []any{version, providerAddr.ForDisplay()} - v.view.streams.Println(v.prepareMessage(ReusingPreviousVersionInfo, params...)) + v.print(v.prepareMessage(ReusingPreviousVersionInfo, params...)) } func (v *InitHuman) LogProviderVersionSuccess(providerAddr addrs.Provider, version getproviders.Version, auth *getproviders.PackageAuthenticationResult) { params := []any{providerAddr.ForDisplay(), version, auth, ""} // add empty key id to the end - v.view.streams.Println(v.prepareMessage(InstalledProviderVersionInfo, params...)) + v.print(v.prepareMessage(InstalledProviderVersionInfo, params...)) } func (v *InitHuman) LogProviderVersionSuccessWithKeyID(providerAddr addrs.Provider, version getproviders.Version, auth *getproviders.PackageAuthenticationResult, keyID string) { keyDetails := fmt.Sprintf(", key ID [reset][bold]%s[reset]", keyID) // key id needs to be formatted for human output params := []any{providerAddr.ForDisplay(), version, auth, keyDetails} - v.view.streams.Println(v.prepareMessage(InstalledProviderVersionInfo, params...)) + v.print(v.prepareMessage(InstalledProviderVersionInfo, params...)) } func (v *InitHuman) LogPartnerAndCommunityProviders() { - v.view.streams.Println(v.prepareMessage(PartnerAndCommunityProvidersMessage)) + v.print(v.prepareMessage(PartnerAndCommunityProvidersMessage)) } // Implements DependencyLockingLogger func (v *InitHuman) LogDependencyLockfileCreated() { params := []any{} - v.view.streams.Println(v.prepareMessage(LockInfo, params...)) + v.print(v.prepareMessage(LockInfo, params...)) } // Implements DependencyLockingLogger func (v *InitHuman) LogDependencyLockfileUpdated() { params := []any{} - v.view.streams.Println(v.prepareMessage(DependenciesLockChangesInfo, params...)) + v.print(v.prepareMessage(DependenciesLockChangesInfo, params...)) } // Implements ModuleInstallationLogger // // See logging in hook_module_install.go func (v *InitHuman) LogModuleDownload(message string) { - v.view.streams.Println(strings.TrimSpace(message)) + v.print(strings.TrimSpace(message)) } // Implements ModuleInstallationLogger // // See logging in hook_module_install.go func (v *InitHuman) LogModuleInstallation(message string) { - v.view.streams.Println(strings.TrimSpace(message)) + v.print(message) } // print formats (trims whitespace & applies colour) and From 34c330475a792935bd0dce10bf6cd8808f8f1a8a Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 5 Aug 2026 11:33:58 +0100 Subject: [PATCH 03/14] refactor: Add `StateStoreProviderTrustLogger` interface that enables a view to log details about establishing trust for a PSS provider This change also takes the PSS-related message codes/types and puts them in the message_types.go file, and avoids the 'message registry' in the init view implementation's file. --- internal/command/meta_backend.go | 8 +- internal/command/views/init.go | 87 ++++++++++++------- internal/command/views/json/message_types.go | 5 ++ internal/command/views/state_migrate.go | 18 +++- .../state_store_provider_trust_logger.go | 25 ++++++ 5 files changed, 107 insertions(+), 36 deletions(-) create mode 100644 internal/command/views/state_store_provider_trust_logger.go diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index 00bc481382d5..a41e1816ba78 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -3110,7 +3110,7 @@ func (m *Meta) determineIfProviderTrusted(provider addrs.Provider, providerLocat // confirmProviderIsTrusted takes the action determined by `determineIfProviderTrusted` and either prompts the user for approval, or returns an error if something has gone wrong with pre-supplied locks when Terraform was run in automation. // // NOTE: the command parameter is used to determine which command is being run, so that we can provide more specific guidance to the user. Do not use that parameter for any other purpose! -func (m *Meta) confirmProviderIsTrusted(trust ProviderTrust, provider addrs.Provider, stateStoreProviderAuthResult *getproviders.PackageAuthenticationResult, stateStoreProviderLock, locksBeforeInstall *depsfile.Locks, flagLockfilePath string, command cli.Command, view views.ProviderInstallationLogger) tfdiags.Diagnostics { +func (m *Meta) confirmProviderIsTrusted(trust ProviderTrust, provider addrs.Provider, stateStoreProviderAuthResult *getproviders.PackageAuthenticationResult, stateStoreProviderLock, locksBeforeInstall *depsfile.Locks, flagLockfilePath string, command cli.Command, view views.StateStoreProviderTrustLogger) tfdiags.Diagnostics { var diags tfdiags.Diagnostics switch trust { @@ -3119,7 +3119,7 @@ func (m *Meta) confirmProviderIsTrusted(trust ProviderTrust, provider addrs.Prov if flagLockfilePath != "" { // If the user supplied a lock file path via CLI flag, we should notify them that it was used. - view.Output(views.StateStoreProviderAutomationApprovedMessage) + view.LogAutomaticApproval() view.Spacer() } case RequiresApproval: @@ -3127,12 +3127,12 @@ func (m *Meta) confirmProviderIsTrusted(trust ProviderTrust, provider addrs.Prov // Prompt the user about trusting the provider used for state storage. diags = diags.Append(m.promptStateStorageProviderApproval(provider, stateStoreProviderLock, stateStoreProviderAuthResult)) if diags.HasErrors() { - view.Output(views.StateStoreProviderInteractiveRejectedMessage) + view.LogInteractiveRejection() view.Spacer() return diags } - view.Output(views.StateStoreProviderInteractiveApprovedMessage) + view.LogInteractiveApproval() view.Spacer() } else { // Confirm that a lock was used to control download. diff --git a/internal/command/views/init.go b/internal/command/views/init.go index fb1ff12210d3..8235413c9e4f 100644 --- a/internal/command/views/init.go +++ b/internal/command/views/init.go @@ -10,6 +10,7 @@ import ( tfaddr "github.com/hashicorp/terraform-registry-address" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command/arguments" + "github.com/hashicorp/terraform/internal/command/views/json" "github.com/hashicorp/terraform/internal/getproviders" "github.com/hashicorp/terraform/internal/policy" "github.com/hashicorp/terraform/internal/tfdiags" @@ -26,6 +27,8 @@ type Init interface { ProviderInstallationLogger DependencyLockingLogger + StateStoreProviderTrustLogger + prepareMessage(messageCode InitMessageCode, params ...any) string Spacer // The `init` command logs empty lines to space-out different sections of human-readable output @@ -87,6 +90,21 @@ func (v *InitHuman) LogInitializingStateStoreProviderPlugin(pAddr tfaddr.Provide v.print(v.prepareMessage(InitializingStateStoreProviderPluginMessage, params...)) } +// Implements StateStoreProviderTrustLogger interface. +func (v *InitHuman) LogInteractiveApproval() { + v.print(logInteractiveApprovalMessageHuman) +} + +// Implements StateStoreProviderTrustLogger interface. +func (v *InitHuman) LogInteractiveRejection() { + v.print(logInteractiveRejectionMessageHuman) +} + +// Implements StateStoreProviderTrustLogger interface. +func (v *InitHuman) LogAutomaticApproval() { + v.print(logInteractiveAutomaticApprovalMessageHuman) +} + func (v *InitHuman) LogFindingMatchingVersion(providerAddr addrs.Provider, versionConstraints getproviders.VersionConstraints) { params := []any{providerAddr.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints)} v.print(v.prepareMessage(FindingMatchingVersionMessage, params...)) @@ -267,6 +285,30 @@ func (v *InitJSON) LogInitializingStateStoreProviderPlugin(pAddr tfaddr.Provider v.Output(InitializingStateStoreProviderPluginMessage, params...) } +// Implements StateStoreProviderTrustLogger interface. +func (v *InitJSON) LogInteractiveApproval() { + v.view.log.Info( + logInteractiveApprovalMessageJSON, + "type", json.StateStoreProviderInteractiveApproval, + ) +} + +// Implements StateStoreProviderTrustLogger interface. +func (v *InitJSON) LogInteractiveRejection() { + v.view.log.Info( + logInteractiveRejectionMessageJSON, + "type", json.StateStoreProviderInteractiveRejection, + ) +} + +// Implements StateStoreProviderTrustLogger interface. +func (v *InitJSON) LogAutomaticApproval() { + v.view.log.Info( + logInteractiveAutomaticApprovalMessageJSON, + "type", json.StateStoreProviderAutomationApproval, + ) +} + func (v *InitJSON) LogFindingMatchingVersion(providerAddr addrs.Provider, versionConstraints getproviders.VersionConstraints) { params := []any{providerAddr.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints)} @@ -453,18 +495,6 @@ var MessageRegistry map[InitMessageCode]InitMessage = map[InitMessageCode]InitMe HumanValue: "\n[reset][bold]Initializing the state store %q...", JSONValue: "Initializing the state store %q...", }, - "state_store_provider_interactive_approved_message": { - HumanValue: "\n[reset][bold]The state store provider was approved by the user.", - JSONValue: "The state store provider was approved by the user.", - }, - "state_store_provider_interactive_rejected_message": { - HumanValue: "\n[reset][bold]The state store provider was rejected by the user.", - JSONValue: "The state store provider was rejected by the user.", - }, - "state_store_provider_automation_approved_message": { - HumanValue: "\n[reset][bold]The state store provider was approved automatically.", - JSONValue: "The state store provider was approved automatically.", - }, "dependencies_lock_changes_info": { HumanValue: dependenciesLockChangesInfo, JSONValue: dependenciesLockChangesInfo, @@ -561,24 +591,21 @@ const ( // Following message codes are used and documented EXTERNALLY // Keep docs/internals/machine-readable-ui.mdx up to date with // this list when making changes here. - CopyingConfigurationMessage InitMessageCode = "copying_configuration_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" - InitializingStateStoreMessage InitMessageCode = "initializing_state_store_message" - InitializingStateStoreProviderPluginMessage InitMessageCode = "initializing_state_store_provider_plugin_message" - StateStoreProviderInteractiveApprovedMessage InitMessageCode = "state_store_provider_interactive_approved_message" - StateStoreProviderInteractiveRejectedMessage InitMessageCode = "state_store_provider_interactive_rejected_message" - StateStoreProviderAutomationApprovedMessage InitMessageCode = "state_store_provider_automation_approved_message" - InitializingProviderPluginMessage InitMessageCode = "initializing_provider_plugin_message" - LockInfo InitMessageCode = "lock_info" - DependenciesLockChangesInfo InitMessageCode = "dependencies_lock_changes_info" + CopyingConfigurationMessage InitMessageCode = "copying_configuration_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" + InitializingStateStoreMessage InitMessageCode = "initializing_state_store_message" + InitializingStateStoreProviderPluginMessage InitMessageCode = "initializing_state_store_provider_plugin_message" + InitializingProviderPluginMessage InitMessageCode = "initializing_provider_plugin_message" + LockInfo InitMessageCode = "lock_info" + DependenciesLockChangesInfo InitMessageCode = "dependencies_lock_changes_info" //// Message codes below are ONLY used INTERNALLY (for now) diff --git a/internal/command/views/json/message_types.go b/internal/command/views/json/message_types.go index 95cad8f93dbc..d23a21197e77 100644 --- a/internal/command/views/json/message_types.go +++ b/internal/command/views/json/message_types.go @@ -63,4 +63,9 @@ const ( MessagePolicyInfo MessageType = "policy_info" MessagePolicyDiagnostic MessageType = "policy_diagnostic" MessagePolicyEvaluationResult MessageType = "policy_result" + + // PSS messages + StateStoreProviderInteractiveApproval MessageType = "state_store_provider_interactive_approval" + StateStoreProviderInteractiveRejection MessageType = "state_store_provider_interactive_rejection" + StateStoreProviderAutomationApproval MessageType = "state_store_provider_automatic_approval" ) diff --git a/internal/command/views/state_migrate.go b/internal/command/views/state_migrate.go index fa92d6756f94..078c387d3451 100644 --- a/internal/command/views/state_migrate.go +++ b/internal/command/views/state_migrate.go @@ -52,6 +52,8 @@ type StateMigrate interface { ProviderInstallationLogger DependencyLockingLogger + StateStoreProviderTrustLogger + Spacer // The `state migrate` command logs empty lines to space-out different sections of human-readable output } @@ -108,8 +110,20 @@ func (s *StateMigrateHuman) LogInitializingStateStoreProviderPlugin(pAddr tfaddr consSuffix = fmt.Sprintf(" (%s)", getproviders.VersionConstraintsString(cons)) } params := []any{pAddr.ForDisplay(), consSuffix, storeType} - msg := s.prepareMessage(InitializingStateStoreProviderPluginMessage, params...) - s.log(msg) + +// Implements StateStoreProviderTrustLogger interface. +func (s *StateMigrateHuman) LogInteractiveApproval() { + s.log(logInteractiveApprovalMessageHuman) +} + +// Implements StateStoreProviderTrustLogger interface. +func (s *StateMigrateHuman) LogInteractiveRejection() { + s.log(logInteractiveRejectionMessageHuman) +} + +// Implements StateStoreProviderTrustLogger interface. +func (s *StateMigrateHuman) LogAutomaticApproval() { + s.log(logInteractiveAutomaticApprovalMessageHuman) } // Implements ProviderInstallationLogger interface. diff --git a/internal/command/views/state_store_provider_trust_logger.go b/internal/command/views/state_store_provider_trust_logger.go new file mode 100644 index 000000000000..1e4e7728ef3e --- /dev/null +++ b/internal/command/views/state_store_provider_trust_logger.go @@ -0,0 +1,25 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package views + +type StateStoreProviderTrustLogger interface { + LogInteractiveApproval() + LogInteractiveRejection() + LogAutomaticApproval() + + Spacer +} + +// Human-readable messages, one version for human-readable output and the 'json' version for the +// human-readable @message field in the JSON output. +const ( + logInteractiveApprovalMessageHuman = "[reset][bold]The state store provider was approved by the user." + logInteractiveApprovalMessageJSON = "The state store provider was approved by the user." + + logInteractiveRejectionMessageHuman = "[reset][bold]The state store provider was rejected by the user." + logInteractiveRejectionMessageJSON = "The state store provider was rejected by the user." + + logInteractiveAutomaticApprovalMessageHuman = "[reset][bold]The state store provider was approved automatically." + logInteractiveAutomaticApprovalMessageJSON = "The state store provider was approved automatically." +) From cb738283b5d42c9725f29fa6e84b0ca4cef6c4c5 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 5 Aug 2026 11:42:25 +0100 Subject: [PATCH 04/14] refactor: Make `LogInitializingStateStoreProviderStart` method no longer use the init message registry when constructing the logged message. Also, update the message type code. Before we matched what was in the init message registry, but now we match the conventions of message_types.go instead of having unnecessary `_message` suffixes. --- internal/command/init.go | 2 +- internal/command/state_migrate.go | 2 +- internal/command/views/init.go | 48 +++++++++---------- internal/command/views/init_test.go | 4 +- internal/command/views/json/message_types.go | 3 ++ .../views/provider_installation_logger.go | 9 +++- internal/command/views/state_migrate.go | 5 +- 7 files changed, 41 insertions(+), 32 deletions(-) diff --git a/internal/command/init.go b/internal/command/init.go index 10f30b599418..15048d257a6a 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -477,7 +477,7 @@ func (c *InitCommand) getProvidersFromPSSConfig(ctx context.Context, rootModEarl // which will be surfaced as diagnostic during installation if !pAddr.IsZero() { cons := reqs[pAddr] - view.LogInitializingStateStoreProviderPlugin(pAddr, cons, rootModEarly.StateStore.Type) + view.LogInitializingStateStoreProviderStart(pAddr, cons, rootModEarly.StateStore.Type) } }, ProviderAlreadyInstalled: providerAlreadyInstalledCallback(view), diff --git a/internal/command/state_migrate.go b/internal/command/state_migrate.go index f62bd052b1a8..0b919ccde40b 100644 --- a/internal/command/state_migrate.go +++ b/internal/command/state_migrate.go @@ -537,7 +537,7 @@ func (c *StateMigrateCommand) getSingleProvider(ctx context.Context, stateStore // which will be surfaced as diagnostic during installation if !pAddr.IsZero() { cons := reqs[pAddr] - view.LogInitializingStateStoreProviderPlugin(pAddr, cons, stateStore.Type) + view.LogInitializingStateStoreProviderStart(pAddr, cons, stateStore.Type) } }, ProviderAlreadyInstalled: providerAlreadyInstalledCallback(view), diff --git a/internal/command/views/init.go b/internal/command/views/init.go index 8235413c9e4f..e1a87cbea41d 100644 --- a/internal/command/views/init.go +++ b/internal/command/views/init.go @@ -81,13 +81,14 @@ func (v *InitHuman) Output(messageCode InitMessageCode, params ...any) { v.print(v.prepareMessage(messageCode, params...)) } -func (v *InitHuman) LogInitializingStateStoreProviderPlugin(pAddr tfaddr.Provider, cons getproviders.VersionConstraints, storeType string) { +func (v *InitHuman) LogInitializingStateStoreProviderStart(pAddr tfaddr.Provider, cons getproviders.VersionConstraints, storeType string) { consSuffix := "" if len(cons) > 0 { consSuffix = fmt.Sprintf(" (%s)", getproviders.VersionConstraintsString(cons)) } params := []any{pAddr.ForDisplay(), consSuffix, storeType} - v.print(v.prepareMessage(InitializingStateStoreProviderPluginMessage, params...)) + msg := fmt.Sprintf(logInitializingStateStoreProviderStartMessageHuman, params...) + v.print(msg) } // Implements StateStoreProviderTrustLogger interface. @@ -273,16 +274,18 @@ func (v *InitJSON) logInitMessage(messageCode InitMessageCode, params ...any) { v.view.Log(preppedMessage) } -func (v *InitJSON) LogInitializingStateStoreProviderPlugin(pAddr tfaddr.Provider, cons getproviders.VersionConstraints, storeType string) { +func (v *InitJSON) LogInitializingStateStoreProviderStart(pAddr tfaddr.Provider, cons getproviders.VersionConstraints, storeType string) { consSuffix := "" if len(cons) > 0 { consSuffix = fmt.Sprintf(" (%s)", getproviders.VersionConstraintsString(cons)) } params := []any{pAddr.ForDisplay(), consSuffix, storeType} + msg := fmt.Sprintf(logInitializingStateStoreProviderStartMessageJSON, params...) - // 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(InitializingStateStoreProviderPluginMessage, params...) + v.view.log.Info( + msg, + "type", json.InitializingStateStoreProviderStart, + ) } // Implements StateStoreProviderTrustLogger interface. @@ -487,10 +490,6 @@ var MessageRegistry map[InitMessageCode]InitMessage = map[InitMessageCode]InitMe HumanValue: "\n[reset][bold]Initializing provider plugins...", JSONValue: "Initializing provider plugins...", }, - "initializing_state_store_provider_plugin_message": { - HumanValue: "\n[reset][bold]Initializing provider %s%s for state store %q...", - JSONValue: "Initializing provider %s%s for state store %q...", - }, "initializing_state_store_message": { HumanValue: "\n[reset][bold]Initializing the state store %q...", JSONValue: "Initializing the state store %q...", @@ -591,21 +590,20 @@ const ( // Following message codes are used and documented EXTERNALLY // Keep docs/internals/machine-readable-ui.mdx up to date with // this list when making changes here. - CopyingConfigurationMessage InitMessageCode = "copying_configuration_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" - InitializingStateStoreMessage InitMessageCode = "initializing_state_store_message" - InitializingStateStoreProviderPluginMessage InitMessageCode = "initializing_state_store_provider_plugin_message" - InitializingProviderPluginMessage InitMessageCode = "initializing_provider_plugin_message" - LockInfo InitMessageCode = "lock_info" - DependenciesLockChangesInfo InitMessageCode = "dependencies_lock_changes_info" + CopyingConfigurationMessage InitMessageCode = "copying_configuration_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" + InitializingStateStoreMessage InitMessageCode = "initializing_state_store_message" + InitializingProviderPluginMessage InitMessageCode = "initializing_provider_plugin_message" + LockInfo InitMessageCode = "lock_info" + DependenciesLockChangesInfo InitMessageCode = "dependencies_lock_changes_info" //// Message codes below are ONLY used INTERNALLY (for now) diff --git a/internal/command/views/init_test.go b/internal/command/views/init_test.go index 0ce789c65da3..98c4624d3c84 100644 --- a/internal/command/views/init_test.go +++ b/internal/command/views/init_test.go @@ -880,7 +880,7 @@ func TestNewInit_LogPartnerAndCommunityProviders_json(t *testing.T) { } } -func TestNewInit_LogInitializingStateStoreProviderPlugin_json(t *testing.T) { +func TestNewInit_LogInitializingStateStoreProviderStart_json(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewView(streams) initView := NewInit(arguments.ViewJSON, view) @@ -888,7 +888,7 @@ func TestNewInit_LogInitializingStateStoreProviderPlugin_json(t *testing.T) { pAddr := addrs.NewDefaultProvider("test") cons := getproviders.MustParseVersionConstraints("~> 1.0") storeType := "test_store" - initView.LogInitializingStateStoreProviderPlugin(pAddr, cons, storeType) + initView.LogInitializingStateStoreProviderStart(pAddr, cons, storeType) // Assert output output := done(t) diff --git a/internal/command/views/json/message_types.go b/internal/command/views/json/message_types.go index d23a21197e77..66d7705b44ec 100644 --- a/internal/command/views/json/message_types.go +++ b/internal/command/views/json/message_types.go @@ -64,6 +64,9 @@ const ( MessagePolicyDiagnostic MessageType = "policy_diagnostic" MessagePolicyEvaluationResult MessageType = "policy_result" + // Provider installation messages + InitializingStateStoreProviderStart MessageType = "state_store_provider_initialization_start" + // PSS messages StateStoreProviderInteractiveApproval MessageType = "state_store_provider_interactive_approval" StateStoreProviderInteractiveRejection MessageType = "state_store_provider_interactive_rejection" diff --git a/internal/command/views/provider_installation_logger.go b/internal/command/views/provider_installation_logger.go index 0ff38e53b35a..07bdf1902b0c 100644 --- a/internal/command/views/provider_installation_logger.go +++ b/internal/command/views/provider_installation_logger.go @@ -45,10 +45,15 @@ type ProviderInstallationLogger interface { // Log that a provider successfully fetched in this operation is maintained by third-parties and describe how these are signed LogPartnerAndCommunityProviders() - // LogInitializingStateStoreProviderPlugin indicates progress during installation of a state store provider plugin - LogInitializingStateStoreProviderPlugin(providerAddr addrs.Provider, cons getproviders.VersionConstraints, storeType string) + // LogInitializingStateStoreProviderStart indicates progress during installation of a state store provider plugin + LogInitializingStateStoreProviderStart(providerAddr addrs.Provider, cons getproviders.VersionConstraints, storeType string) prepareMessage(messageCode InitMessageCode, params ...any) string Spacer // output from provider installation is spaced out from following human-readable output log lines } + +const ( + logInitializingStateStoreProviderStartMessageHuman = "[reset][bold]Initializing provider %s%s for state store %q..." + logInitializingStateStoreProviderStartMessageJSON = "Initializing provider %s%s for state store %q..." +) diff --git a/internal/command/views/state_migrate.go b/internal/command/views/state_migrate.go index 078c387d3451..cfa084e53e70 100644 --- a/internal/command/views/state_migrate.go +++ b/internal/command/views/state_migrate.go @@ -104,12 +104,15 @@ func (s *StateMigrateHuman) Output(code InitMessageCode, params ...any) { } // Implements ProviderInstallationLogger interface. -func (s *StateMigrateHuman) LogInitializingStateStoreProviderPlugin(pAddr tfaddr.Provider, cons getproviders.VersionConstraints, storeType string) { +func (s *StateMigrateHuman) LogInitializingStateStoreProviderStart(pAddr tfaddr.Provider, cons getproviders.VersionConstraints, storeType string) { consSuffix := "" if len(cons) > 0 { consSuffix = fmt.Sprintf(" (%s)", getproviders.VersionConstraintsString(cons)) } params := []any{pAddr.ForDisplay(), consSuffix, storeType} + msg := fmt.Sprintf(logInitializingStateStoreProviderStartMessageHuman, params...) + s.log(msg) +} // Implements StateStoreProviderTrustLogger interface. func (s *StateMigrateHuman) LogInteractiveApproval() { From 0ca1ae63bd70ce81964352392cb797b6a189fa60 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 5 Aug 2026 14:07:05 +0100 Subject: [PATCH 05/14] test: Update test to reflect the new `state_store_provider_initialization_start` message type, and how it's used as the value of the `type` field in JSON objects. --- internal/command/views/init_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/command/views/init_test.go b/internal/command/views/init_test.go index 98c4624d3c84..6d5aee771db6 100644 --- a/internal/command/views/init_test.go +++ b/internal/command/views/init_test.go @@ -897,8 +897,7 @@ func TestNewInit_LogInitializingStateStoreProviderStart_json(t *testing.T) { `"@message":"Initializing provider hashicorp/test (~\u003e 1.0) for state store \"test_store\"..."`, `"@module":"terraform.ui"`, //@timestamp is dynamic - `"message_code":"initializing_state_store_provider_plugin_message"`, - `"type":"init_output"`, + `"type":"state_store_provider_initialization_start"`, } for _, snippet := range expectedOutputFields { if !strings.Contains(output.Stdout(), snippet) { From 856c5e9b4c93fc303a6ca776b4c44fa7ba72753c Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 15 Jul 2026 15:08:37 +0100 Subject: [PATCH 06/14] refactor: Update stat migrate view's `prepareMessage` method to panic if an unrecognised message code, or empty message contents, is encountered. The `init` implementation of 'prepareMessage' doesn't panic and instead lets data fall through, which potentially can cover up defects (they'd otherwise only be detectable by tests asserting that log's contents). As `state migrate` is new I believe panicking is good as it helps us identify defects. --- internal/command/views/state_migrate.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/command/views/state_migrate.go b/internal/command/views/state_migrate.go index cfa084e53e70..9f5aadd9dff5 100644 --- a/internal/command/views/state_migrate.go +++ b/internal/command/views/state_migrate.go @@ -214,8 +214,7 @@ func (s *StateMigrateHuman) LogDependencyLockfileUpdated() { func (s *StateMigrateHuman) prepareMessage(code InitMessageCode, params ...any) string { message, ok := MessageRegistry[code] if !ok { - // display the message code as fallback if not found in the message registry - return string(code) + panic("missing message for init message code " + string(code)) } if message.HumanValue == "" { From e718209338ea487f35776270da8cd9a18589c1e1 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 15 Jul 2026 15:26:37 +0100 Subject: [PATCH 07/14] feat: Add `StateMigrateJSON` view implementing Spacer interface --- internal/command/views/state_migrate.go | 19 +++++++++++++++++-- internal/command/views/state_migrate_test.go | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/internal/command/views/state_migrate.go b/internal/command/views/state_migrate.go index 9f5aadd9dff5..638885e71b18 100644 --- a/internal/command/views/state_migrate.go +++ b/internal/command/views/state_migrate.go @@ -60,9 +60,13 @@ type StateMigrate interface { func NewStateMigrate(viewType arguments.ViewType, view *View) StateMigrate { switch viewType { case arguments.ViewHuman: - return &StateMigrateHuman{view: view} + return &StateMigrateHuman{ + view: view, + } default: - panic(fmt.Sprintf("unsupported view type: %s", viewType)) + return &StateMigrateJSON{ + view: NewJSONView(view), + } } } @@ -224,3 +228,14 @@ func (s *StateMigrateHuman) prepareMessage(code InitMessageCode, params ...any) return s.view.colorize.Color(strings.TrimSpace(fmt.Sprintf(message.HumanValue, params...))) } + +var _ Spacer = (*StateMigrateJSON)(nil) + +type StateMigrateJSON struct { + view *JSONView +} + +// Implements Spacer +func (s *StateMigrateJSON) Spacer() { + // no-op for JSON output, since we don't want to log empty messages in JSON +} diff --git a/internal/command/views/state_migrate_test.go b/internal/command/views/state_migrate_test.go index a57b1cf6c3ca..657091acc552 100644 --- a/internal/command/views/state_migrate_test.go +++ b/internal/command/views/state_migrate_test.go @@ -97,3 +97,21 @@ func TestNewStateMigrate_LogProviderVersionSuccessWithKeyID(t *testing.T) { } }) } + +func TestNewStateMigrate_Spacer_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + smView := NewStateMigrate(arguments.ViewJSON, view) + + smView.Spacer() + + // Assert output + output := done(t) + + // We cannot simply assert no output as the JSON view logs the version message on initialization + // Splitting on \n when there's only the version log will get an array of the log and an empty string. + // If there are more logs there'll be >2 elements. + if x := strings.Split(output.Stdout(), "\n"); len(x) != 2 { + t.Fatalf("expected no additional output after version message, got: %s", output.Stdout()) + } +} From 095bc5d2d75160f57c650f9451226d1806a4e82e Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 15 Jul 2026 15:33:34 +0100 Subject: [PATCH 08/14] feat: Add `Log` method to `StateMigrateJSON` view --- internal/command/views/state_migrate.go | 15 ++++++++++++++ internal/command/views/state_migrate_test.go | 21 ++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/internal/command/views/state_migrate.go b/internal/command/views/state_migrate.go index 638885e71b18..ed9f2e96f26e 100644 --- a/internal/command/views/state_migrate.go +++ b/internal/command/views/state_migrate.go @@ -84,10 +84,12 @@ func (s *StateMigrateHuman) Diagnostics(diags tfdiags.Diagnostics) { s.view.Diagnostics(diags) } +// Plain logging of messages, without using message codes func (s *StateMigrateHuman) Log(message string, params ...any) { s.log(fmt.Sprintf(message, params...)) } +// log is reused to ensure human output is always trimmed and colourised before printing to the output stream. func (s *StateMigrateHuman) log(preparedMessage string) { msg := s.view.colorize.Color(strings.TrimSpace(preparedMessage)) s.view.streams.Println(msg) @@ -239,3 +241,16 @@ type StateMigrateJSON struct { func (s *StateMigrateJSON) Spacer() { // no-op for JSON output, since we don't want to log empty messages in JSON } + +// Plain logging of messages +// Logged data includes by default: +// @level as "info" +// @module as "terraform.ui" (See NewJSONView) +// @timestamp formatted in the default way +// type as "log". +// +// No additional fields supplied. +func (s *StateMigrateJSON) Log(message string, params ...any) { + msg := strings.TrimSpace(fmt.Sprintf(message, params...)) + s.view.log.Info(msg) +} diff --git a/internal/command/views/state_migrate_test.go b/internal/command/views/state_migrate_test.go index 657091acc552..c76c88a85410 100644 --- a/internal/command/views/state_migrate_test.go +++ b/internal/command/views/state_migrate_test.go @@ -98,6 +98,27 @@ func TestNewStateMigrate_LogProviderVersionSuccessWithKeyID(t *testing.T) { }) } +func TestNewStateMigrate_Log_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + smView := NewStateMigrate(arguments.ViewJSON, view) + + templateMessage := "This is a test log message with a parameter (%s), and both trailing whitespace and newline. \n" + parameter := "test_parameter" + smView.Log(templateMessage, parameter) + + // Assert output + output := done(t) + expected := fmt.Sprintf( + `"@message":"%s"`, + strings.TrimSpace(fmt.Sprintf(templateMessage, parameter)), + ) + + if !strings.Contains(output.Stdout(), expected) { + t.Fatalf("output didn't include expected snippet:\n expected: %s\n got:\n %s", expected, output.Stdout()) + } +} + func TestNewStateMigrate_Spacer_json(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewView(streams) From 0f668b525defdc0b618417e8a33bce67449f8842 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 15 Jul 2026 15:37:39 +0100 Subject: [PATCH 09/14] feat: Add `InstalledProviderVersionInfo` and `InstalledProviderVersionInfoWithKeyID` methods to `StateMigrateJSON` view --- internal/command/views/state_migrate.go | 23 ++++++ internal/command/views/state_migrate_test.go | 77 ++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/internal/command/views/state_migrate.go b/internal/command/views/state_migrate.go index ed9f2e96f26e..fe9f19a3bbc4 100644 --- a/internal/command/views/state_migrate.go +++ b/internal/command/views/state_migrate.go @@ -254,3 +254,26 @@ func (s *StateMigrateJSON) Log(message string, params ...any) { msg := strings.TrimSpace(fmt.Sprintf(message, params...)) s.view.log.Info(msg) } + +// Implements ProviderInstaller interface. +func (s *StateMigrateJSON) InstalledProviderVersionInfo(providerAddr addrs.Provider, version getproviders.Version, auth *getproviders.PackageAuthenticationResult) { + params := []any{providerAddr.ForDisplay(), version, auth, ""} // add empty key id to the end + msg := s.prepareMessage(InstalledProviderVersionInfo, params...) + s.view.log.Info( + msg, + "type", InstalledProviderVersionInfo, + ) +} + +// Implements ProviderInstaller interface. +func (s *StateMigrateJSON) InstalledProviderVersionInfoWithKeyID(providerAddr addrs.Provider, version getproviders.Version, auth *getproviders.PackageAuthenticationResult, keyID string) { + keyDetails := fmt.Sprintf("key_id: %s", keyID) // key id needs to be formatted for JSON output + params := []any{providerAddr.ForDisplay(), version, auth, keyDetails} + + msg := s.prepareMessage(InstalledProviderVersionInfo, params...) + s.view.log.Info( + msg, + "type", InstalledProviderVersionInfo, + ) +} + diff --git a/internal/command/views/state_migrate_test.go b/internal/command/views/state_migrate_test.go index c76c88a85410..9d416c749c47 100644 --- a/internal/command/views/state_migrate_test.go +++ b/internal/command/views/state_migrate_test.go @@ -4,6 +4,8 @@ package views import ( + "fmt" + "strings" "testing" "github.com/hashicorp/terraform/internal/addrs" @@ -35,6 +37,24 @@ func TestNewStateMigrate_LogProviderVersionSuccess(t *testing.T) { t.Fatalf("expected %q, got %q", expectedOutput, output.Stdout()) } }) + t.Run("no auth result - json view", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + smView := NewStateMigrate(arguments.ViewJSON, view) + + p := addrs.MustParseProviderSourceString("hashicorp/test") + ver := getproviders.MustParseVersion("1.2.3") + var authResult *getproviders.PackageAuthenticationResult = nil + + smView.InstalledProviderVersionInfo(p, ver, authResult) + + // Assert output + output := done(t) + expectedOutput := `"@message":"Installed provider version: hashicorp/test v1.2.3 (unauthenticated)"` + if !strings.Contains(output.Stdout(), expectedOutput) { + t.Fatalf("output didn't include expected snippet:\n expected: %s\n got:\n %s", expectedOutput, output.Stdout()) + } + }) t.Run("verified checksum auth result - human view", func(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewView(streams) @@ -53,6 +73,24 @@ func TestNewStateMigrate_LogProviderVersionSuccess(t *testing.T) { t.Fatalf("expected %q, got %q", expectedOutput, output.Stdout()) } }) + t.Run("verified checksum auth result - json view", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + smView := NewStateMigrate(arguments.ViewJSON, view) + + p := addrs.MustParseProviderSourceString("hashicorp/test") + ver := getproviders.MustParseVersion("1.2.3") + authResult := getproviders.NewPackageAuthenticationResult(verifiedChecksum, noKey) + + smView.InstalledProviderVersionInfo(p, ver, authResult) + + // Assert output - human + output := done(t) + expectedOutput := `"@message":"Installed provider version: hashicorp/test v1.2.3 (verified checksum)"` + if !strings.Contains(output.Stdout(), expectedOutput) { + t.Fatalf("output didn't include expected snippet:\n expected: %s\n got:\n %s", expectedOutput, output.Stdout()) + } + }) t.Run("official provider auth result - human view", func(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewView(streams) @@ -72,8 +110,28 @@ func TestNewStateMigrate_LogProviderVersionSuccess(t *testing.T) { t.Fatalf("expected %q, got %q", expectedOutput, output.Stdout()) } }) + t.Run("official provider auth result - json view", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + smView := NewStateMigrate(arguments.ViewJSON, view) + + p := addrs.MustParseProviderSourceString("hashicorp/test") + ver := getproviders.MustParseVersion("1.2.3") + key := "key-id-123" + authResult := getproviders.NewPackageAuthenticationResult(officialProvider, key) + + smView.InstalledProviderVersionInfo(p, ver, authResult) + + // Assert output - human + output := done(t) + expectedOutput := `"@message":"Installed provider version: hashicorp/test v1.2.3 (signed by HashiCorp)"` + if !strings.Contains(output.Stdout(), expectedOutput) { + t.Fatalf("output didn't include expected snippet:\n expected: %s\n got:\n %s", expectedOutput, output.Stdout()) + } + }) } +// Assert message content func TestNewStateMigrate_LogProviderVersionSuccessWithKeyID(t *testing.T) { const partnerProvider = 2 @@ -96,6 +154,25 @@ func TestNewStateMigrate_LogProviderVersionSuccessWithKeyID(t *testing.T) { t.Fatalf("expected %q, got %q", expectedOutput, output.Stdout()) } }) + t.Run("partner provider auth result -json view", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + smView := NewStateMigrate(arguments.ViewJSON, view) + + p := addrs.MustParseProviderSourceString("hashicorp/test") + ver := getproviders.MustParseVersion("1.2.3") + key := "key-id-123" + authResult := getproviders.NewPackageAuthenticationResult(partnerProvider, key) + + smView.InstalledProviderVersionInfoWithKeyID(p, ver, authResult, key) + + // Assert output + output := done(t) + expectedOutput := `"@message":"Installed provider version: hashicorp/test v1.2.3 (signed by a HashiCorp partnerkey_id: key-id-123)"` + if !strings.Contains(output.Stdout(), expectedOutput) { + t.Fatalf("output didn't include expected snippet:\n expected: %s\n got:\n %s", expectedOutput, output.Stdout()) + } + }) } func TestNewStateMigrate_Log_json(t *testing.T) { From c75374310bf9cfbb942675caff7a6addcbae7dbe Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 15 Jul 2026 15:39:12 +0100 Subject: [PATCH 10/14] feat: Fully implement `StateMigrate` and `ProviderInstaller` interfaces on `StateMigrateJSON` view --- internal/command/views/state_migrate.go | 130 ++++++++- internal/command/views/state_migrate_test.go | 279 ++++++++++++++++++- 2 files changed, 402 insertions(+), 7 deletions(-) diff --git a/internal/command/views/state_migrate.go b/internal/command/views/state_migrate.go index fe9f19a3bbc4..01e49b3bd502 100644 --- a/internal/command/views/state_migrate.go +++ b/internal/command/views/state_migrate.go @@ -10,6 +10,7 @@ import ( tfaddr "github.com/hashicorp/terraform-registry-address" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command/arguments" + "github.com/hashicorp/terraform/internal/command/views/json" "github.com/hashicorp/terraform/internal/getproviders" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -231,12 +232,20 @@ func (s *StateMigrateHuman) prepareMessage(code InitMessageCode, params ...any) return s.view.colorize.Color(strings.TrimSpace(fmt.Sprintf(message.HumanValue, params...))) } -var _ Spacer = (*StateMigrateJSON)(nil) +var ( + _ StateMigrate = (*StateMigrateJSON)(nil) + _ ProviderInstallationLogger = (*StateMigrateJSON)(nil) + _ Spacer = (*StateMigrateJSON)(nil) +) type StateMigrateJSON struct { view *JSONView } +func (s *StateMigrateJSON) Diagnostics(diags tfdiags.Diagnostics) { + s.view.Diagnostics(diags) +} + // Implements Spacer func (s *StateMigrateJSON) Spacer() { // no-op for JSON output, since we don't want to log empty messages in JSON @@ -256,7 +265,101 @@ func (s *StateMigrateJSON) Log(message string, params ...any) { } // Implements ProviderInstaller interface. -func (s *StateMigrateJSON) InstalledProviderVersionInfo(providerAddr addrs.Provider, version getproviders.Version, auth *getproviders.PackageAuthenticationResult) { +func (s *StateMigrateJSON) Output(code InitMessageCode, params ...any) { + msg, ok := MessageRegistry[code] + if !ok || msg.JSONValue == "" { + panic(fmt.Sprintf("missing message for init message code %s, got: %s", string(code), msg.JSONValue)) + } + s.Log(msg.JSONValue, params...) +} + +// Implements ProviderInstallationLogger interface. +func (s *StateMigrateJSON) LogInitializingStateStoreProviderStart(providerAddr addrs.Provider, cons getproviders.VersionConstraints, storeType string) { + consSuffix := "" + if len(cons) > 0 { + consSuffix = fmt.Sprintf(" (%s)", getproviders.VersionConstraintsString(cons)) + } + params := []any{providerAddr.ForDisplay(), consSuffix, storeType} + msg := fmt.Sprintf(logInitializingStateStoreProviderStartMessageJSON, params...) + + s.view.log.Info( + msg, + "type", json.InitializingStateStoreProviderStart, + ) +} + +// Implements ProviderInstaller interface. +func (s *StateMigrateJSON) LogFindingMatchingVersion(providerAddr addrs.Provider, versionConstraints getproviders.VersionConstraints) { + params := []any{providerAddr.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints)} + msg := s.prepareMessage(FindingMatchingVersionMessage, params...) + s.view.log.Info( + msg, + "type", FindingMatchingVersionMessage, + ) +} + +// Implements ProviderInstaller interface. +func (s *StateMigrateJSON) LogFindingLatestVersion(providerAddr addrs.Provider) { + params := []any{providerAddr.ForDisplay()} + msg := s.prepareMessage(FindingLatestVersionMessage, params...) + s.view.log.Info( + msg, + "type", FindingLatestVersionMessage, + ) +} + +// Implements ProviderInstaller interface. +func (s *StateMigrateJSON) LogProviderVersionAlreadyInstalled(providerAddr addrs.Provider, version getproviders.Version) { + params := []any{providerAddr.ForDisplay(), version} + msg := s.prepareMessage(ProviderAlreadyInstalledMessage, params...) + s.view.log.Info( + msg, + "type", ProviderAlreadyInstalledMessage, + ) +} + +// Implements ProviderInstaller interface. +func (s *StateMigrateJSON) LogUsingProviderVersionFromCacheDir(providerAddr addrs.Provider, version getproviders.Version) { + params := []any{providerAddr.ForDisplay(), version} + msg := s.prepareMessage(UsingProviderFromCacheDirInfo, params...) + s.view.log.Info( + msg, + "type", UsingProviderFromCacheDirInfo, + ) +} + +// Implements ProviderInstaller interface. +func (s *StateMigrateJSON) LogBuiltInProviderAvailable(providerAddr addrs.Provider) { + params := []any{providerAddr.ForDisplay()} + msg := s.prepareMessage(BuiltInProviderAvailableMessage, params...) + s.view.log.Info( + msg, + "type", BuiltInProviderAvailableMessage, + ) +} + +// Implements ProviderInstaller interface. +func (s *StateMigrateJSON) LogInstallingProviderVersion(providerAddr addrs.Provider, version getproviders.Version) { + params := []any{providerAddr.ForDisplay(), version} + msg := s.prepareMessage(InstallingProviderMessage, params...) + s.view.log.Info( + msg, + "type", InstallingProviderMessage, + ) +} + +// Implements ProviderInstaller interface. +func (s *StateMigrateJSON) LogReusingPreviousProviderVersion(providerAddr addrs.Provider, version getproviders.Version) { + params := []any{providerAddr.ForDisplay(), version.String()} + msg := s.prepareMessage(ReusingPreviousVersionInfo, params...) + s.view.log.Info( + msg, + "type", ReusingPreviousVersionInfo, + ) +} + +// Implements ProviderInstaller interface. +func (s *StateMigrateJSON) LogProviderVersionSuccess(providerAddr addrs.Provider, version getproviders.Version, auth *getproviders.PackageAuthenticationResult) { params := []any{providerAddr.ForDisplay(), version, auth, ""} // add empty key id to the end msg := s.prepareMessage(InstalledProviderVersionInfo, params...) s.view.log.Info( @@ -266,7 +369,7 @@ func (s *StateMigrateJSON) InstalledProviderVersionInfo(providerAddr addrs.Provi } // Implements ProviderInstaller interface. -func (s *StateMigrateJSON) InstalledProviderVersionInfoWithKeyID(providerAddr addrs.Provider, version getproviders.Version, auth *getproviders.PackageAuthenticationResult, keyID string) { +func (s *StateMigrateJSON) LogProviderVersionSuccessWithKeyID(providerAddr addrs.Provider, version getproviders.Version, auth *getproviders.PackageAuthenticationResult, keyID string) { keyDetails := fmt.Sprintf("key_id: %s", keyID) // key id needs to be formatted for JSON output params := []any{providerAddr.ForDisplay(), version, auth, keyDetails} @@ -277,3 +380,24 @@ func (s *StateMigrateJSON) InstalledProviderVersionInfoWithKeyID(providerAddr ad ) } +// Implements ProviderInstaller interface. +func (s *StateMigrateJSON) LogPartnerAndCommunityProviders() { + msg := s.prepareMessage(PartnerAndCommunityProvidersMessage) + s.view.log.Info( + msg, + "type", PartnerAndCommunityProvidersMessage, + ) +} + +// Implements ProviderInstaller interface. +func (s *StateMigrateJSON) prepareMessage(code InitMessageCode, params ...any) string { + message, ok := MessageRegistry[code] + if !ok || message.JSONValue == "" { + // We neither want to log a non-existent message code, nor an empty message in JSON. + // Empty messages should only be intentionally logged via the Spacer method, which is a no-op in the JSON view. + + panic(fmt.Sprintf("missing message for init message code %s, got: %s", string(code), message.JSONValue)) + } + + return strings.TrimSpace(fmt.Sprintf(message.JSONValue, params...)) +} diff --git a/internal/command/views/state_migrate_test.go b/internal/command/views/state_migrate_test.go index 9d416c749c47..177c83154aad 100644 --- a/internal/command/views/state_migrate_test.go +++ b/internal/command/views/state_migrate_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/apparentlymart/go-versions/versions" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/getproviders" @@ -46,7 +47,7 @@ func TestNewStateMigrate_LogProviderVersionSuccess(t *testing.T) { ver := getproviders.MustParseVersion("1.2.3") var authResult *getproviders.PackageAuthenticationResult = nil - smView.InstalledProviderVersionInfo(p, ver, authResult) + smView.LogProviderVersionSuccess(p, ver, authResult) // Assert output output := done(t) @@ -82,7 +83,7 @@ func TestNewStateMigrate_LogProviderVersionSuccess(t *testing.T) { ver := getproviders.MustParseVersion("1.2.3") authResult := getproviders.NewPackageAuthenticationResult(verifiedChecksum, noKey) - smView.InstalledProviderVersionInfo(p, ver, authResult) + smView.LogProviderVersionSuccess(p, ver, authResult) // Assert output - human output := done(t) @@ -120,7 +121,7 @@ func TestNewStateMigrate_LogProviderVersionSuccess(t *testing.T) { key := "key-id-123" authResult := getproviders.NewPackageAuthenticationResult(officialProvider, key) - smView.InstalledProviderVersionInfo(p, ver, authResult) + smView.LogProviderVersionSuccess(p, ver, authResult) // Assert output - human output := done(t) @@ -164,7 +165,7 @@ func TestNewStateMigrate_LogProviderVersionSuccessWithKeyID(t *testing.T) { key := "key-id-123" authResult := getproviders.NewPackageAuthenticationResult(partnerProvider, key) - smView.InstalledProviderVersionInfoWithKeyID(p, ver, authResult, key) + smView.LogProviderVersionSuccessWithKeyID(p, ver, authResult, key) // Assert output output := done(t) @@ -175,6 +176,276 @@ func TestNewStateMigrate_LogProviderVersionSuccessWithKeyID(t *testing.T) { }) } +// Assert JSON log content, including log type and additional fields +func TestNewStateMigrate_LogProviderVersionSuccess_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + smView := NewStateMigrate(arguments.ViewJSON, view) + + p := addrs.MustParseProviderSourceString("hashicorp/test") + v := versions.MustParseVersion("1.0.0") + officialProvider := 1 + authResult := getproviders.NewPackageAuthenticationResult(officialProvider, "key-id-123") + smView.LogProviderVersionSuccess(p, v, authResult) + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"Installed provider version: hashicorp/test v1.0.0 (signed by HashiCorp)"`, + `"@module":"terraform.ui"`, + `"type":"installed_provider_version_info"`, + } + 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 TestNewStateMigrate_ProviderAlreadyInstalled_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + smView := NewStateMigrate(arguments.ViewJSON, view) + + p := addrs.MustParseProviderSourceString("hashicorp/test") + v := versions.MustParseVersion("1.0.0") + smView.LogProviderVersionAlreadyInstalled(p, v) + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"hashicorp/test v1.0.0: Using previously-installed provider version"`, + `"@module":"terraform.ui"`, + `"type":"provider_already_installed_message"`, + } + 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()) + } + } +} + +// Assert JSON log content, including log type and additional fields +// +// Note - in calling code this is only ever used for partner providers +func TestNewStateMigrate_LogProviderVersionSuccessWithKeyID_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + smView := NewStateMigrate(arguments.ViewJSON, view) + + p := addrs.MustParseProviderSourceString("hashicorp/test") + v := versions.MustParseVersion("1.0.0") + partnerProvider := 2 + keyID := "key-id-123" + authResult := getproviders.NewPackageAuthenticationResult(partnerProvider, keyID) + smView.LogProviderVersionSuccessWithKeyID(p, v, authResult, keyID) + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"Installed provider version: hashicorp/test v1.0.0 (signed by a HashiCorp partnerkey_id: key-id-123)"`, + `"@module":"terraform.ui"`, + `"type":"installed_provider_version_info"`, + } + 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 TestNewStateMigrate_ReusingPreviousVersion_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + initView := NewInit(arguments.ViewJSON, view) + + p := addrs.MustParseProviderSourceString("hashicorp/test") + v := versions.MustParseVersion("1.0.0") + initView.LogReusingPreviousProviderVersion(p, v) + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"hashicorp/test: Reusing previous version from the dependency lock file"`, + `"@module":"terraform.ui"`, + `"type":"log"`, + } + 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 TestNewStateMigrate_FindingMatchingVersion_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + smView := NewStateMigrate(arguments.ViewJSON, view) + + p := addrs.MustParseProviderSourceString("hashicorp/test") + constraint, _ := getproviders.ParseVersionConstraints("1.0.0") + smView.LogFindingMatchingVersion(p, constraint) + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"Finding matching versions for provider: hashicorp/test, version_constraint: \"1.0.0\""`, + `"@module":"terraform.ui"`, + `"type":"finding_matching_version_message"`, + } + 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 TestNewStateMigrate_FindingLatestVersion_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + smView := NewStateMigrate(arguments.ViewJSON, view) + + p := addrs.MustParseProviderSourceString("hashicorp/test") + smView.LogFindingLatestVersion(p) + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"hashicorp/test: Finding latest version..."`, + `"@module":"terraform.ui"`, + `"type":"finding_latest_version_message"`, + } + 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 TestNewStateMigrate_InstallingProvider_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + smView := NewStateMigrate(arguments.ViewJSON, view) + + p := addrs.MustParseProviderSourceString("hashicorp/test") + v := versions.MustParseVersion("1.0.0") + smView.LogInstallingProviderVersion(p, v) + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"Installing provider version: hashicorp/test v1.0.0..."`, + `"@module":"terraform.ui"`, + `"type":"installing_provider_message"`, + } + 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 TestNewStateMigrate_BuiltInProviderAvailable_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + smView := NewStateMigrate(arguments.ViewJSON, view) + + p := addrs.MustParseProviderSourceString("hashicorp/test") + smView.LogBuiltInProviderAvailable(p) + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"hashicorp/test is built in to Terraform"`, + `"@module":"terraform.ui"`, + `"type":"built_in_provider_available_message"`, + } + 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 TestNewStateMigrate_UsingProviderFromCacheDirInfo_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + smView := NewStateMigrate(arguments.ViewJSON, view) + + p := addrs.MustParseProviderSourceString("hashicorp/test") + v := versions.MustParseVersion("1.0.0") + smView.LogUsingProviderVersionFromCacheDir(p, v) + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"hashicorp/test v1.0.0: Using from the shared cache directory"`, + `"@module":"terraform.ui"`, + `"type":"using_provider_from_cache_dir_info"`, + } + 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 TestNewStateMigrate_PartnerAndCommunityProviders_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + smView := NewStateMigrate(arguments.ViewJSON, view) + + smView.LogPartnerAndCommunityProviders() + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"Partner and community providers are signed by their developers.\nIf you'd like to know more about provider signing, you can read about it here:\nhttps://developer.hashicorp.com/terraform/cli/plugins/signing"`, + `"@module":"terraform.ui"`, + `"type":"partner_and_community_providers_message"`, + } + 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 TestNewStateMigrate_InitializingStateStoreProviderPlugin_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + smView := NewStateMigrate(arguments.ViewJSON, view) + + p := addrs.MustParseProviderSourceString("hashicorp/test") + vc := getproviders.MustParseVersionConstraints("~> 1.1") + storeType := "test_store" + smView.LogInitializingStateStoreProviderStart(p, vc, storeType) + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"Initializing provider hashicorp/test (~\u003e 1.1) for state store \"test_store\"..."`, + `"@module":"terraform.ui"`, + `"type":"state_store_provider_initialization_start"`, + } + 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 TestNewStateMigrate_Log_json(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewView(streams) From 3e20708d4d1bc9e16801ddf18353e207bd28229c Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 20 Jul 2026 19:50:04 +0100 Subject: [PATCH 11/14] test: Add test helper for making assertions about JSON output in a parameterised way --- internal/command/command_test.go | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/internal/command/command_test.go b/internal/command/command_test.go index db29b4ae1c30..fb8b0cc2792a 100644 --- a/internal/command/command_test.go +++ b/internal/command/command_test.go @@ -1293,6 +1293,8 @@ func checkGoldenReferenceHumanOutput(t *testing.T, output *terminal.TestOutput, // checkParameterizedGoldenReferenceHumanOutput compares a test fixture's log output with the given test output. // The log is expected to be in a file called "output.log" or "output-parameterized.log" located under the specified fixture path. // +// If any parameters are provided, this function expects to find and use a "output-parameterized.jsonlog" file. +// // The log can contain format specifiers that will be replaced with the given params, and these are only intended // for use when output references values like current platform or Terraform version. func checkParameterizedGoldenReferenceHumanOutput(t *testing.T, output *terminal.TestOutput, fixturePathName string, params ...interface{}) { @@ -1343,8 +1345,30 @@ func checkParameterizedGoldenReferenceHumanOutput(t *testing.T, output *terminal func checkGoldenReference(t *testing.T, output *terminal.TestOutput, fixturePathName string) { t.Helper() + // No params + checkParameterizedGoldenReference(t, output, fixturePathName) +} + +// checkParameterizedGoldenReference compares a test fixture's log output with the given test JSON output. +// The log is expected to be in a file called "output.jsonlog" or "output-parameterized.jsonlog" located under the specified fixture path. +// +// If any parameters are provided, this function expects to find and use a "output-parameterized.jsonlog" file. +// +// The log can contain format specifiers that will be replaced with the given params, and these are only intended +// for use when output references values like current platform or Terraform version. +func checkParameterizedGoldenReference(t *testing.T, output *terminal.TestOutput, fixturePathName string, params ...interface{}) { + t.Helper() + + var expectedFilePath string + + if len(params) > 0 { + expectedFilePath = path.Join(testFixturePath(fixturePathName), "output-parameterized.jsonlog") + } else { + expectedFilePath = path.Join(testFixturePath(fixturePathName), "output.jsonlog") + } + // Load the golden reference fixture - wantFile, err := os.Open(path.Join(testFixturePath(fixturePathName), "output.jsonlog")) + wantFile, err := os.Open(expectedFilePath) if err != nil { t.Fatalf("failed to open output file: %s", err) } @@ -1353,7 +1377,8 @@ func checkGoldenReference(t *testing.T, output *terminal.TestOutput, fixturePath if err != nil { t.Fatalf("failed to read output file: %s", err) } - want := string(wantBytes) + wantTemplate := string(wantBytes) + want := fmt.Sprintf(wantTemplate, params...) checkGoldenReferenceStr(t, output, want) } From c5a285fd05eaf4fa0cd98af02a18ca4096bea0aa Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 20 Jul 2026 19:50:34 +0100 Subject: [PATCH 12/14] test: Add integration tests asserting that command JSON output matches golden reference files. --- internal/command/state_migrate_test.go | 393 ++++++++++++------ .../output.jsonlog | 3 + .../output.jsonlog | 7 + .../output-parameterized.jsonlog | 13 + 4 files changed, 293 insertions(+), 123 deletions(-) create mode 100644 internal/command/testdata/state-migrate-backend-to-backend/output.jsonlog create mode 100644 internal/command/testdata/state-migrate-backend-to-state-store/output.jsonlog create mode 100644 internal/command/testdata/state-store-changed/provider-used/output-parameterized.jsonlog diff --git a/internal/command/state_migrate_test.go b/internal/command/state_migrate_test.go index 8c37d8ca408d..9a60b2f5497c 100644 --- a/internal/command/state_migrate_test.go +++ b/internal/command/state_migrate_test.go @@ -24,150 +24,224 @@ import ( ) func TestStateMigrate_fromBackendToBackend(t *testing.T) { - fixture := "state-migrate-backend-to-backend" - wd := tempWorkingDirFixture(t, fixture) - t.Chdir(wd.RootModuleDir()) + t.Run("full assertions, including asserting human view output", func(t *testing.T) { + fixture := "state-migrate-backend-to-backend" + wd := tempWorkingDirFixture(t, fixture) + t.Chdir(wd.RootModuleDir()) - ui := testUiWrapped(t) - view, done := testView(t) - c := &StateMigrateCommand{ - Meta: Meta{ - Ui: ui, - View: view, - WorkingDir: wd, - AllowExperimentalFeatures: true, - }, - } + ui := testUiWrapped(t) + view, done := testView(t) + c := &StateMigrateCommand{ + Meta: Meta{ + Ui: ui, + View: view, + WorkingDir: wd, + AllowExperimentalFeatures: true, + }, + } - _ = testInputMap(t, map[string]string{ - "backend-migrate-copy-to-empty": "yes", - }) + _ = testInputMap(t, map[string]string{ + "backend-migrate-copy-to-empty": "yes", + }) - args := []string{"-no-color"} - code := c.Run(args) - out := done(t) - if code != 0 { - t.Fatalf("expected exit code 1, got %d\nstderr: %q", code, out.Stderr()) - } + args := []string{"-no-color"} + code := c.Run(args) + out := done(t) + if code != 0 { + t.Fatalf("expected exit code 0, got %d\nstderr: %q", code, out.Stderr()) + } - // Assert expected human output is made - checkGoldenReferenceHumanOutput(t, out, fixture) + // Assert expected human output is made + checkGoldenReferenceHumanOutput(t, out, fixture) - // Assert the migrated state contains expected content - f, err := os.Open("destination-backend.tfstate") - if err != nil { - t.Fatalf("failed to read migrated state: %s", err) - } - t.Cleanup(func() { - err := f.Close() + // Assert the migrated state contains expected content + f, err := os.Open("destination-backend.tfstate") + if err != nil { + t.Fatalf("failed to read migrated state: %s", err) + } + t.Cleanup(func() { + err := f.Close() + if err != nil { + t.Fatal(err) + } + }) + s, err := statefile.Read(f) if err != nil { t.Fatal(err) } + _, ok := s.State.RootOutputValues["test"] + if !ok { + t.Fatalf("unable to find test output in migrated state") + } + + // Assert the backend state file describes the new backend location, + statePath := filepath.Join(c.DataDir(), DefaultStateFilename) + sMgr := &clistate.LocalState{Path: statePath} + if err := sMgr.RefreshState(); err != nil { + t.Fatal(err) + } + backendState := sMgr.State() + if backendState.StateStore != nil { + t.Fatalf("expected backend state file to not describe a state_store during backend=>backend, but it's set") + } + if backendState.Backend == nil { + t.Fatalf("expected backend state file to describe a backend during backend=>backend, but it's nil") + } + if backendState.Backend.Type != "local" { + t.Fatalf("expected backend state file to describe the destination backend \"local\", but got %q", backendState.Backend.Type) + } + if got, want := normalizeJSON(t, backendState.Backend.ConfigRaw), `{"path":"destination-backend.tfstate","workspace_dir":null}`; got != want { + t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) + } }) - s, err := statefile.Read(f) - if err != nil { - t.Fatal(err) - } - _, ok := s.State.RootOutputValues["test"] - if !ok { - t.Fatalf("unable to find test output in migrated state") - } + t.Run("assert only JSON view output", func(t *testing.T) { + fixture := "state-migrate-backend-to-backend" + wd := tempWorkingDirFixture(t, fixture) + t.Chdir(wd.RootModuleDir()) - // Assert the backend state file describes the new backend location, - statePath := filepath.Join(c.DataDir(), DefaultStateFilename) - sMgr := &clistate.LocalState{Path: statePath} - if err := sMgr.RefreshState(); err != nil { - t.Fatal(err) - } - backendState := sMgr.State() - if backendState.StateStore != nil { - t.Fatalf("expected backend state file to not describe a state_store during backend=>backend, but it's set") - } - if backendState.Backend == nil { - t.Fatalf("expected backend state file to describe a backend during backend=>backend, but it's nil") - } - if backendState.Backend.Type != "local" { - t.Fatalf("expected backend state file to describe the destination backend \"local\", but got %q", backendState.Backend.Type) - } - if got, want := normalizeJSON(t, backendState.Backend.ConfigRaw), `{"path":"destination-backend.tfstate","workspace_dir":null}`; got != want { - t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) - } + ui := testUiWrapped(t) + view, done := testView(t) + c := &StateMigrateCommand{ + Meta: Meta{ + Ui: ui, + View: view, + WorkingDir: wd, + AllowExperimentalFeatures: true, + }, + } + + args := []string{ + "-input=false", + "-json", + "-force-copy", // deal with interactive prompts to approve the migration + } + code := c.Run(args) + out := done(t) + if code != 0 { + t.Fatalf("expected exit code 0, got %d\nstdout: %q\nstderr: %q", code, out.Stdout(), out.Stderr()) + } + + // Assert expected JSON output is made + checkGoldenReference(t, out, fixture) + + // No further assertions; done in previous sub test + }) } func TestStateMigrate_fromBackendToStateStore(t *testing.T) { - fixture := "state-migrate-backend-to-state-store" - wd := tempWorkingDirFixture(t, fixture) - t.Chdir(wd.RootModuleDir()) + t.Run("full assertions, including asserting human view output", func(t *testing.T) { + fixture := "state-migrate-backend-to-state-store" + wd := tempWorkingDirFixture(t, fixture) + t.Chdir(wd.RootModuleDir()) - p := mockPluggableStateStorageProvider(mockSingleStateStoreSchema("test_store")) - p.MockStates = testing_provider.NewMockStateBytesWithStateIds("test_store", []string{"default"}) - providerSource := newMockProviderSource(t, map[string][]string{ - "hashicorp/test": {"1.2.3"}, - }) + p := mockPluggableStateStorageProvider(mockSingleStateStoreSchema("test_store")) + p.MockStates = testing_provider.NewMockStateBytesWithStateIds("test_store", []string{"default"}) + providerSource := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.2.3"}, + }) - ui := testUiWrapped(t) - view, done := testView(t) - c := &StateMigrateCommand{ - Meta: Meta{ - Ui: ui, - View: view, - WorkingDir: wd, - AllowExperimentalFeatures: true, - testingOverrides: metaOverridesForProvider(p), - ProviderSource: providerSource, - }, - } + ui := testUiWrapped(t) + view, done := testView(t) + c := &StateMigrateCommand{ + Meta: Meta{ + Ui: ui, + View: view, + WorkingDir: wd, + AllowExperimentalFeatures: true, + testingOverrides: metaOverridesForProvider(p), + ProviderSource: providerSource, + }, + } - _ = testInputMap(t, map[string]string{ - "backend-migrate-copy-to-empty": "yes", + _ = testInputMap(t, map[string]string{ + "backend-migrate-copy-to-empty": "yes", + }) + + args := []string{"-no-color"} + code := c.Run(args) + out := done(t) + if code != 0 { + t.Fatalf("unexpected exit: %d\nstderr: %q", code, out.Stderr()) + } + + // Assert expected human output is made + checkGoldenReferenceHumanOutput(t, out, fixture) + + // Assert the migrated state contains expected content + b, err := p.MockStates.Read("test_store", "default") + if err != nil { + t.Fatalf("unable to find migrated state in mock provider: %s", err) + } + s, err := statefile.Read(bytes.NewBuffer(b)) + if err != nil { + t.Fatal(err) + } + _, ok := s.State.RootOutputValues["test"] + if !ok { + t.Fatalf("unable to find test output in migrated state") + } + + // Assert the backend state file describes the new state store location. + statePath := filepath.Join(c.DataDir(), DefaultStateFilename) + sMgr := &clistate.LocalState{Path: statePath} + if err := sMgr.RefreshState(); err != nil { + t.Fatal(err) + } + backendState := sMgr.State() + if backendState.Backend != nil { + t.Fatalf("expected backend state file to not describe a backend during backend=>state_store migration, but it's set") + } + if backendState.StateStore == nil { + t.Fatalf("expected backend state file to describe a state store during backend=>state_store migration, but it's nil") + } + if backendState.StateStore.Provider.Source.Type != "test" { + t.Fatalf("expected backend state file to describe the destination state store provider \"test\", but got %q", backendState.StateStore.Provider) + } + if backendState.StateStore.Provider.Version.String() != "1.2.3" { + t.Fatalf("expected backend state file to describe the destination state store provider version \"1.2.3\", but got %q", backendState.StateStore.Provider.Version) + } + if backendState.StateStore.Type != "test_store" { + t.Fatalf("expected backend state file to describe the destination state store type \"test_store\", but got %q", backendState.StateStore.Type) + } }) + t.Run("assert only JSON view output", func(t *testing.T) { + fixture := "state-migrate-backend-to-state-store" + wd := tempWorkingDirFixture(t, fixture) + t.Chdir(wd.RootModuleDir()) - args := []string{"-no-color"} - code := c.Run(args) - out := done(t) - if code != 0 { - t.Fatalf("unexpected exit: %d\nstderr: %q", code, out.Stderr()) - } + p := mockPluggableStateStorageProvider(mockSingleStateStoreSchema("test_store")) + p.MockStates = testing_provider.NewMockStateBytesWithStateIds("test_store", []string{"default"}) + providerSource := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.2.3"}, + }) - // Assert expected human output is made - checkGoldenReferenceHumanOutput(t, out, fixture) + ui := testUiWrapped(t) + view, done := testView(t) + c := &StateMigrateCommand{ + Meta: Meta{ + Ui: ui, + View: view, + WorkingDir: wd, + AllowExperimentalFeatures: true, + testingOverrides: metaOverridesForProvider(p), + ProviderSource: providerSource, + }, + } - // Assert the migrated state contains expected content - b, err := p.MockStates.Read("test_store", "default") - if err != nil { - t.Fatalf("unable to find migrated state in mock provider: %s", err) - } - s, err := statefile.Read(bytes.NewBuffer(b)) - if err != nil { - t.Fatal(err) - } - _, ok := s.State.RootOutputValues["test"] - if !ok { - t.Fatalf("unable to find test output in migrated state") - } + args := []string{ + "-input=false", + "-json", + "-force-copy", // deal with interactive prompts to approve the migration + } + code := c.Run(args) + out := done(t) + if code != 0 { + t.Fatalf("unexpected exit: %d\nstderr: %q", code, out.Stderr()) + } - // Assert the backend state file describes the new state store location. - statePath := filepath.Join(c.DataDir(), DefaultStateFilename) - sMgr := &clistate.LocalState{Path: statePath} - if err := sMgr.RefreshState(); err != nil { - t.Fatal(err) - } - backendState := sMgr.State() - if backendState.Backend != nil { - t.Fatalf("expected backend state file to not describe a backend during backend=>state_store migration, but it's set") - } - if backendState.StateStore == nil { - t.Fatalf("expected backend state file to describe a state store during backend=>state_store migration, but it's nil") - } - if backendState.StateStore.Provider.Source.Type != "test" { - t.Fatalf("expected backend state file to describe the destination state store provider \"test\", but got %q", backendState.StateStore.Provider) - } - if backendState.StateStore.Provider.Version.String() != "1.2.3" { - t.Fatalf("expected backend state file to describe the destination state store provider version \"1.2.3\", but got %q", backendState.StateStore.Provider.Version) - } - if backendState.StateStore.Type != "test_store" { - t.Fatalf("expected backend state file to describe the destination state store type \"test_store\", but got %q", backendState.StateStore.Type) - } + // Assert expected human output is made + checkGoldenReference(t, out, fixture) + }) } // Testing migration between two state stores in a single provider. @@ -542,6 +616,79 @@ provider "registry.terraform.io/hashicorp/test2" { // Assert the backend state file describes the new state store location. assertBackendStateFile(t, c) }) + t.Run("assert only JSON view output: source provider already in the dependency lock file, destination is not", func(t *testing.T) { + fixture := "state-store-changed/provider-used" + wd := tempWorkingDirFixture(t, fixture) + t.Chdir(wd.RootModuleDir()) + + b, err := os.ReadFile("source-pss.tfstate") + if err != nil { + t.Fatal(err) + } + + // hashicorp/test + sourcePssSchema := map[string]providers.Schema{ + "test_src": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{}, + }, + }, + } + sourceProvider := mockPluggableStateStorageProvider(sourcePssSchema) + sourceProvider.MockStates = testing_provider.MockStateBytes{ + "test_src": map[string][]byte{"default": []byte(b)}, + } + // hashicorp/test2 + destinationPssSchema := map[string]providers.Schema{ + "test2_store": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{}, + }, + }, + } + destinationProvider := mockPluggableStateStorageProvider(destinationPssSchema) + destinationProvider.MockStates = testing_provider.MockStateBytes{ + "test2_store": map[string][]byte{}, // No existing state in the destination + } + providerSource := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.2.3"}, + "hashicorp/test2": {"3.2.1"}, + }) + + ui := testUiWrapped(t) + view, done := testView(t) + c := &StateMigrateCommand{ + Meta: Meta{ + Ui: ui, + View: view, + WorkingDir: wd, + AllowExperimentalFeatures: true, + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): providers.FactoryFixed(sourceProvider), + addrs.NewDefaultProvider("test2"): providers.FactoryFixed(destinationProvider), + }, + }, + ProviderSource: providerSource, + }, + } + + args := []string{ + "-input=false", + "-json", + "-force-copy", // deal with interactive prompts to approve the migration + } + code := c.Run(args) + out := done(t) + if code != 0 { + t.Fatalf("unexpected exit: %d\nstderr: %q", code, out.Stderr()) + } + + // Assert expected JSON output is made + // Parameterized due to output referencing the current platform. + checkParameterizedGoldenReference(t, out, fixture, getproviders.CurrentPlatform.String()) + }) + t.Run("destination provider already in the dependency lock file, source is not", func(t *testing.T) { wd := tempWorkingDirFixture(t, "state-store-changed/provider-used") t.Chdir(wd.RootModuleDir()) diff --git a/internal/command/testdata/state-migrate-backend-to-backend/output.jsonlog b/internal/command/testdata/state-migrate-backend-to-backend/output.jsonlog new file mode 100644 index 000000000000..c0740a4a22bc --- /dev/null +++ b/internal/command/testdata/state-migrate-backend-to-backend/output.jsonlog @@ -0,0 +1,3 @@ +{"@level":"info","@message":"Terraform 1.16.0-dev","@module":"terraform.ui","@timestamp":"2026-07-20T17:37:42.389757+01:00","terraform":"1.16.0-dev","type":"version","ui":"1.3"} +{"@level":"info","@message":"[reset][bold]Migrating state from backend \"local\" to backend \"local\"...[reset]","@module":"terraform.ui","@timestamp":"2026-07-20T17:37:42.392448+01:00"} +{"@level":"info","@message":"[reset][bold]Finished migrating state from backend \"local\" to backend \"local\".[reset]","@module":"terraform.ui","@timestamp":"2026-07-20T17:37:42.402661+01:00"} \ No newline at end of file diff --git a/internal/command/testdata/state-migrate-backend-to-state-store/output.jsonlog b/internal/command/testdata/state-migrate-backend-to-state-store/output.jsonlog new file mode 100644 index 000000000000..cfc4ce917c11 --- /dev/null +++ b/internal/command/testdata/state-migrate-backend-to-state-store/output.jsonlog @@ -0,0 +1,7 @@ +{"@level":"info","@message":"Terraform 1.16.0-dev","@module":"terraform.ui","@timestamp":"2026-07-20T18:24:15.987666+01:00","terraform":"1.16.0-dev","type":"version","ui":"1.3"} +{"@level":"info","@message":"Initializing provider hashicorp/test (1.2.3) for state store \"test_store\"...","@module":"terraform.ui","@timestamp":"2026-07-20T18:24:19.353736+01:00","type":"state_store_provider_initialization_start"} +{"@level":"info","@message":"hashicorp/test: Reusing version 1.2.3 from the dependency lock file","@module":"terraform.ui","@timestamp":"2026-07-20T18:24:19.353950+01:00","type":"reusing_previous_version_info"} +{"@level":"info","@message":"Installing provider version: hashicorp/test v1.2.3...","@module":"terraform.ui","@timestamp":"2026-07-20T18:24:19.354164+01:00","type":"installing_provider_message"} +{"@level":"info","@message":"Installed provider version: hashicorp/test v1.2.3 (verified checksum)","@module":"terraform.ui","@timestamp":"2026-07-20T18:24:19.357634+01:00","type":"installed_provider_version_info"} +{"@level":"info","@message":"[reset][bold]Migrating state from backend \"local\" to state store \"test_store\" (hashicorp/test 1.2.3)...[reset]","@module":"terraform.ui","@timestamp":"2026-07-20T18:24:23.501107+01:00"} +{"@level":"info","@message":"[reset][bold]Finished migrating state from backend \"local\" to state store \"test_store\" (hashicorp/test 1.2.3).[reset]","@module":"terraform.ui","@timestamp":"2026-07-20T18:24:23.510351+01:00"} diff --git a/internal/command/testdata/state-store-changed/provider-used/output-parameterized.jsonlog b/internal/command/testdata/state-store-changed/provider-used/output-parameterized.jsonlog new file mode 100644 index 000000000000..2337716a911b --- /dev/null +++ b/internal/command/testdata/state-store-changed/provider-used/output-parameterized.jsonlog @@ -0,0 +1,13 @@ +{"@level":"info","@message":"Terraform 1.16.0-dev","@module":"terraform.ui","@timestamp":"2026-07-20T19:11:22.803855+01:00","terraform":"1.16.0-dev","type":"version","ui":"1.3"} +{"@level":"info","@message":"Initializing provider plugin for state store \"test_src\"...","@module":"terraform.ui","@timestamp":"2026-07-20T19:11:22.806751+01:00","type":"initializing_state_store_provider_plugin_message"} +{"@level":"info","@message":"hashicorp/test: Reusing previous version from the dependency lock file","@module":"terraform.ui","@timestamp":"2026-07-20T19:11:22.806774+01:00","type":"reusing_previous_version_info"} +{"@level":"info","@message":"Installing provider version: hashicorp/test v1.2.3...","@module":"terraform.ui","@timestamp":"2026-07-20T19:11:22.806837+01:00","type":"installing_provider_message"} +{"@level":"info","@message":"Installed provider version: hashicorp/test v1.2.3 (verified checksum)","@module":"terraform.ui","@timestamp":"2026-07-20T19:11:22.807983+01:00","type":"installed_provider_version_info"} +{"@level":"info","@message":"Initializing provider plugin for state store \"test2_store\"...","@module":"terraform.ui","@timestamp":"2026-07-20T19:11:22.808229+01:00","type":"initializing_state_store_provider_plugin_message"} +{"@level":"info","@message":"hashicorp/test2: Finding latest version...","@module":"terraform.ui","@timestamp":"2026-07-20T19:11:22.808240+01:00","type":"finding_latest_version_message"} +{"@level":"info","@message":"Installing provider version: hashicorp/test2 v3.2.1...","@module":"terraform.ui","@timestamp":"2026-07-20T19:11:22.808531+01:00","type":"installing_provider_message"} +{"@level":"info","@message":"Installed provider version: hashicorp/test2 v3.2.1 (verified checksum)","@module":"terraform.ui","@timestamp":"2026-07-20T19:11:22.809792+01:00","type":"installed_provider_version_info"} +{"@level":"info","@message":"[reset][bold]Migrating state from state store \"test_src\" (hashicorp/test) to state store \"test2_store\" (hashicorp/test2)...[reset]","@module":"terraform.ui","@timestamp":"2026-07-20T19:11:22.809984+01:00"} +{"@level":"info","@message":"Terraform has made some changes to the provider dependency selections recorded\nin the .terraform.lock.hcl file. Review those changes and commit them to your\nversion control system if they represent changes you intended to make.","@module":"terraform.ui","@timestamp":"2026-07-20T19:11:22.810446+01:00"} +{"@level":"warn","@message":"Warning: Incomplete lock file information for providers","@module":"terraform.ui","@timestamp":"2026-07-20T19:11:22.815998+01:00","diagnostic":{"severity":"warning","summary":"Incomplete lock file information for providers","detail":"Due to your customized provider installation methods, Terraform was forced to calculate lock file checksums locally for the following providers:\n - hashicorp/test\n - hashicorp/test2\n\nThe current .terraform.lock.hcl file only includes checksums for %s, so Terraform running on another platform will fail to install these providers.\n\nTo calculate additional checksums for another platform, run:\n terraform providers lock -platform=linux_amd64\n(where linux_amd64 is the platform to generate)"},"type":"diagnostic"} +{"@level":"info","@message":"[reset][bold]Finished migrating state from state store \"test_src\" (hashicorp/test) to state store \"test2_store\" (hashicorp/test2).[reset]","@module":"terraform.ui","@timestamp":"2026-07-20T19:11:22.816430+01:00"} \ No newline at end of file From 5dc77c26c67357ed5f4c337c24627d877ac8af9a Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 5 Aug 2026 15:18:44 +0100 Subject: [PATCH 13/14] feat: Implement `StateStoreProviderTrustLogger` on JSON view for `state migrate` --- internal/command/views/state_migrate.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/internal/command/views/state_migrate.go b/internal/command/views/state_migrate.go index 01e49b3bd502..0fd39d1890a6 100644 --- a/internal/command/views/state_migrate.go +++ b/internal/command/views/state_migrate.go @@ -288,6 +288,30 @@ func (s *StateMigrateJSON) LogInitializingStateStoreProviderStart(providerAddr a ) } +// Implements StateStoreProviderTrustLogger interface. +func (s *StateMigrateJSON) LogInteractiveApproval() { + s.view.log.Info( + logInteractiveApprovalMessageJSON, + "type", json.StateStoreProviderInteractiveApproval, + ) +} + +// Implements StateStoreProviderTrustLogger interface. +func (s *StateMigrateJSON) LogInteractiveRejection() { + s.view.log.Info( + logInteractiveRejectionMessageJSON, + "type", json.StateStoreProviderInteractiveRejection, + ) +} + +// Implements StateStoreProviderTrustLogger interface. +func (s *StateMigrateJSON) LogAutomaticApproval() { + s.view.log.Info( + logInteractiveAutomaticApprovalMessageJSON, + "type", json.StateStoreProviderAutomationApproval, + ) +} + // Implements ProviderInstaller interface. func (s *StateMigrateJSON) LogFindingMatchingVersion(providerAddr addrs.Provider, versionConstraints getproviders.VersionConstraints) { params := []any{providerAddr.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints)} From e7830bfdd6a82b116c9d87cbc437a72c2a1d7913 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 5 Aug 2026 15:22:35 +0100 Subject: [PATCH 14/14] feat: Implement `DependencyLockingLogger` on JSON view for `state migrate` --- internal/command/views/json/message_types.go | 2 ++ internal/command/views/state_migrate.go | 20 ++++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/internal/command/views/json/message_types.go b/internal/command/views/json/message_types.go index 66d7705b44ec..313f025ddd7a 100644 --- a/internal/command/views/json/message_types.go +++ b/internal/command/views/json/message_types.go @@ -66,6 +66,8 @@ const ( // Provider installation messages InitializingStateStoreProviderStart MessageType = "state_store_provider_initialization_start" + DependencyLockFileCreated MessageType = "dependency_lockfile_created" + DependencyLockFileUpdated MessageType = "dependency_lockfile_updated" // PSS messages StateStoreProviderInteractiveApproval MessageType = "state_store_provider_interactive_approval" diff --git a/internal/command/views/state_migrate.go b/internal/command/views/state_migrate.go index 0fd39d1890a6..9219cfc6eafc 100644 --- a/internal/command/views/state_migrate.go +++ b/internal/command/views/state_migrate.go @@ -207,12 +207,12 @@ func (s *StateMigrateHuman) LogPartnerAndCommunityProviders() { s.log(msg) } -// Implements DependencyLockLogger interface. +// Implements DependencyLockingLogger interface. func (s *StateMigrateHuman) LogDependencyLockfileCreated() { s.log(previousLockInfoHuman) } -// Implements DependencyLockLogger interface. +// Implements DependencyLockingLogger interface. func (s *StateMigrateHuman) LogDependencyLockfileUpdated() { s.log(dependenciesLockChangesInfo) } @@ -413,6 +413,22 @@ func (s *StateMigrateJSON) LogPartnerAndCommunityProviders() { ) } +// Implements DependencyLockingLogger interface. +func (s *StateMigrateJSON) LogDependencyLockfileCreated() { + s.view.log.Info( + previousLockInfoJSON, + "type", json.DependencyLockFileCreated, + ) +} + +// Implements DependencyLockingLogger interface. +func (s *StateMigrateJSON) LogDependencyLockfileUpdated() { + s.view.log.Info( + dependenciesLockChangesInfo, + "type", json.DependencyLockFileUpdated, + ) +} + // Implements ProviderInstaller interface. func (s *StateMigrateJSON) prepareMessage(code InitMessageCode, params ...any) string { message, ok := MessageRegistry[code]