Files
opentf/internal/command/views/state.go
Andrei Ciobanu 8689efa1f1 Add backend view and migrate outputs (#3949)
Signed-off-by: Andrei Ciobanu <andrei.ciobanu@opentofu.org>
2026-03-30 15:35:09 +03:00

578 lines
16 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package views
import (
"context"
"fmt"
"os"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/command/arguments"
"github.com/opentofu/opentofu/internal/command/jsonformat"
"github.com/opentofu/opentofu/internal/command/jsonprovider"
"github.com/opentofu/opentofu/internal/command/jsonstate"
"github.com/opentofu/opentofu/internal/states"
"github.com/opentofu/opentofu/internal/states/statefile"
"github.com/opentofu/opentofu/internal/tfdiags"
"github.com/opentofu/opentofu/internal/tofu"
)
type State interface {
Diagnostics(diags tfdiags.Diagnostics)
// General `state` output
StateNotFound()
StateLoadingFailure(baseError string)
StateSavingError(baseError string)
// `tofu state list` specific
StateListAddr(resAddr addrs.AbsResourceInstance)
// `tofu state mv` specific
ErrorMovingToAlreadyExistingDst()
ResourceMoveStatus(dryRun bool, src, dest string)
DryRunMovedStatus(moved int)
MoveFinalStatus(moved int)
// `tofu state pull` specific
PrintPulledState(state string)
// `tofu state replace-provider` specific
NoMatchingResourcesForProviderReplacement()
ReplaceProviderOverview(from, to addrs.Provider, willReplace []*states.Resource)
ReplaceProviderCancelled()
ProviderReplaced(forResources int)
// `tofu state rm` specific
ResourceRemoveStatus(dryRun bool, target string)
DryRunRemovedStatus(removed int)
RemoveFinalStatus(count int)
// `tofu state show` specific
UnsupportedLocalOp()
AddressParsingError(resAddr string)
NoInstanceFoundError()
ShowResourceState(ctx context.Context, stateFile *statefile.File, schemas *tofu.Schemas) int
// Backend returns the non-command view that contains methods to provide
// progress output for the backend operations.
Backend() Backend
}
// NewState returns an initialized State implementation for the given ViewType.
func NewState(args arguments.ViewOptions, view *View) State {
var ret State
switch args.ViewType {
case arguments.ViewJSON:
ret = &StateJSON{view: NewJSONView(view, nil), output: view.streams.Stdout.File}
case arguments.ViewHuman:
ret = &StateHuman{view: view}
default:
panic(fmt.Sprintf("unknown view type %v", args.ViewType))
}
if args.JSONInto != nil {
ret = &StateMulti{ret, &StateJSON{view: NewJSONView(view, args.JSONInto), output: args.JSONInto}}
}
return ret
}
type StateMulti []State
var _ State = (StateMulti)(nil)
func (m StateMulti) Diagnostics(diags tfdiags.Diagnostics) {
for _, o := range m {
o.Diagnostics(diags)
}
}
func (m StateMulti) StateNotFound() {
for _, o := range m {
o.StateNotFound()
}
}
func (m StateMulti) StateLoadingFailure(baseError string) {
for _, o := range m {
o.StateLoadingFailure(baseError)
}
}
func (m StateMulti) StateSavingError(baseError string) {
for _, o := range m {
o.StateSavingError(baseError)
}
}
func (m StateMulti) StateListAddr(resAddr addrs.AbsResourceInstance) {
for _, o := range m {
o.StateListAddr(resAddr)
}
}
func (m StateMulti) ErrorMovingToAlreadyExistingDst() {
for _, o := range m {
o.ErrorMovingToAlreadyExistingDst()
}
}
func (m StateMulti) ResourceMoveStatus(dryRun bool, src, dest string) {
for _, o := range m {
o.ResourceMoveStatus(dryRun, src, dest)
}
}
func (m StateMulti) DryRunMovedStatus(moved int) {
for _, o := range m {
o.DryRunMovedStatus(moved)
}
}
func (m StateMulti) MoveFinalStatus(moved int) {
for _, o := range m {
o.MoveFinalStatus(moved)
}
}
func (m StateMulti) PrintPulledState(state string) {
for _, o := range m {
o.PrintPulledState(state)
}
}
func (m StateMulti) NoMatchingResourcesForProviderReplacement() {
for _, o := range m {
o.NoMatchingResourcesForProviderReplacement()
}
}
func (m StateMulti) ReplaceProviderOverview(from, to addrs.Provider, willReplace []*states.Resource) {
for _, o := range m {
o.ReplaceProviderOverview(from, to, willReplace)
}
}
func (m StateMulti) ReplaceProviderCancelled() {
for _, o := range m {
o.ReplaceProviderCancelled()
}
}
func (m StateMulti) ProviderReplaced(forResources int) {
for _, o := range m {
o.ProviderReplaced(forResources)
}
}
func (m StateMulti) ResourceRemoveStatus(dryRun bool, target string) {
for _, o := range m {
o.ResourceRemoveStatus(dryRun, target)
}
}
func (m StateMulti) DryRunRemovedStatus(removed int) {
for _, o := range m {
o.DryRunRemovedStatus(removed)
}
}
func (m StateMulti) RemoveFinalStatus(count int) {
for _, o := range m {
o.RemoveFinalStatus(count)
}
}
func (m StateMulti) UnsupportedLocalOp() {
for _, o := range m {
o.UnsupportedLocalOp()
}
}
func (m StateMulti) AddressParsingError(resAddr string) {
for _, o := range m {
o.AddressParsingError(resAddr)
}
}
func (m StateMulti) NoInstanceFoundError() {
for _, o := range m {
o.NoInstanceFoundError()
}
}
func (m StateMulti) ShowResourceState(ctx context.Context, stateFile *statefile.File, schemas *tofu.Schemas) int {
var ret int
for _, o := range m {
ret = max(ret, o.ShowResourceState(ctx, stateFile, schemas))
}
return ret
}
func (m StateMulti) Backend() Backend {
ret := make([]Backend, len(m))
for i, v := range m {
ret[i] = v.Backend()
}
return BackendMulti(ret)
}
type StateHuman struct {
view *View
}
var _ State = (*StateHuman)(nil)
func (v *StateHuman) Diagnostics(diags tfdiags.Diagnostics) {
v.view.Diagnostics(diags)
}
func (v *StateHuman) StateNotFound() {
v.Diagnostics(tfdiags.Diagnostics{diagErrStateNotFound})
}
func (v *StateHuman) StateLoadingFailure(baseError string) {
v.Diagnostics(tfdiags.Diagnostics{tfdiags.Sourceless(
tfdiags.Error,
errStateLoadingStateSummary,
fmt.Sprintf(errStateLoadingStateDescription, baseError),
)})
}
func (v *StateHuman) StateSavingError(baseError string) {
v.Diagnostics(tfdiags.Diagnostics{tfdiags.Sourceless(
tfdiags.Error,
errStateRmPersistHeader,
fmt.Sprintf(errStateRmPersistDescription, baseError),
)})
}
func (v *StateHuman) StateListAddr(resAddr addrs.AbsResourceInstance) {
_, _ = v.view.streams.Println(resAddr.String())
}
func (v *StateHuman) ErrorMovingToAlreadyExistingDst() {
v.Diagnostics(tfdiags.Diagnostics{diagErrStateMvDstExists})
}
func (v *StateHuman) ResourceMoveStatus(dryRun bool, src, dest string) {
if dryRun {
_, _ = v.view.streams.Println(fmt.Sprintf("Would move %q to %q", src, dest))
return
}
_, _ = v.view.streams.Println(fmt.Sprintf("Move %q to %q", src, dest))
}
func (v *StateHuman) DryRunMovedStatus(moved int) {
if moved == 0 {
_, _ = v.view.streams.Println("Would have moved nothing.")
}
}
func (v *StateHuman) MoveFinalStatus(moved int) {
if moved == 0 {
_, _ = v.view.streams.Println("No matching objects found.")
return
}
_, _ = v.view.streams.Println(fmt.Sprintf("Successfully moved %d object(s).", moved))
}
func (v *StateHuman) PrintPulledState(state string) {
_, _ = v.view.streams.Println(state)
}
func (v *StateHuman) NoMatchingResourcesForProviderReplacement() {
_, _ = v.view.streams.Println("No matching resources found.")
}
func (v *StateHuman) ReplaceProviderOverview(from, to addrs.Provider, willReplace []*states.Resource) {
colorize := v.view.colorize.Color
printer := func(args ...any) { _, _ = v.view.streams.Println(args...) }
printer("OpenTofu will perform the following actions:\n")
printer(colorize(" [yellow]~[reset] Updating provider:"))
printer(colorize(fmt.Sprintf(" [red]-[reset] %s", from)))
printer(colorize(fmt.Sprintf(" [green]+[reset] %s\n", to)))
printer(colorize(fmt.Sprintf("[bold]Changing[reset] %d resources:\n", len(willReplace))))
for _, resource := range willReplace {
printer(colorize(fmt.Sprintf(" %s", resource.Addr)))
}
}
func (v *StateHuman) ReplaceProviderCancelled() {
_, _ = v.view.streams.Println("Cancelled replacing providers.")
}
func (v *StateHuman) ProviderReplaced(forResources int) {
_, _ = v.view.streams.Println(fmt.Sprintf("Successfully replaced provider for %d resources.", forResources))
}
func (v *StateHuman) ResourceRemoveStatus(dryRun bool, target string) {
if dryRun {
_, _ = v.view.streams.Println(fmt.Sprintf("Would remove %s", target))
return
}
_, _ = v.view.streams.Println(fmt.Sprintf("Removed %s", target))
}
func (v *StateHuman) DryRunRemovedStatus(removed int) {
if removed == 0 {
_, _ = v.view.streams.Println("Would have removed nothing.")
}
}
func (v *StateHuman) RemoveFinalStatus(count int) {
if count == 0 {
// NOTE: printing nothing here since this case needs to be handled by the caller
return
}
_, _ = v.view.streams.Println(fmt.Sprintf("Successfully removed %d resource instance(s).", count))
}
func (v *StateHuman) UnsupportedLocalOp() {
v.Diagnostics(tfdiags.Diagnostics{diagUnsupportedLocalOp})
}
func (v *StateHuman) AddressParsingError(resAddr string) {
v.Diagnostics(tfdiags.Diagnostics{tfdiags.Sourceless(
tfdiags.Error,
fmt.Sprintf(errParsingAddressHeader, resAddr),
errParsingAddressDescription,
)})
}
func (v *StateHuman) NoInstanceFoundError() {
v.Diagnostics(tfdiags.Diagnostics{diagErrNoInstanceFound})
}
func (v *StateHuman) ShowResourceState(_ context.Context, stateFile *statefile.File, schemas *tofu.Schemas) int {
renderer := jsonformat.Renderer{
Colorize: v.view.colorize,
Streams: v.view.streams,
RunningInAutomation: v.view.runningInAutomation,
ShowSensitive: v.view.showSensitive,
}
if stateFile == nil {
_, _ = v.view.streams.Println("No state.")
return 0
}
root, outputs, err := jsonstate.MarshalForRenderer(stateFile, schemas)
if err != nil {
v.Diagnostics(tfdiags.Diagnostics{}.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to marshal state to json",
fmt.Sprintf("Error while marshalling state to json: %s", err),
)))
return 1
}
jstate := jsonformat.State{
StateFormatVersion: jsonstate.FormatVersion,
ProviderFormatVersion: jsonprovider.FormatVersion,
RootModule: root,
RootModuleOutputs: outputs,
ProviderSchemas: jsonprovider.MarshalForRenderer(schemas),
}
renderer.RenderHumanState(jstate)
return 0
}
func (v *StateHuman) Backend() Backend {
return &BackendHuman{
view: v.view,
}
}
type StateJSON struct {
view *JSONView
output *os.File
}
var _ State = (*StateJSON)(nil)
func (v *StateJSON) Diagnostics(diags tfdiags.Diagnostics) {
v.view.Diagnostics(diags)
}
func (v *StateJSON) StateNotFound() {
v.Diagnostics(tfdiags.Diagnostics{diagErrStateNotFound})
}
func (v *StateJSON) StateLoadingFailure(baseError string) {
v.Diagnostics(tfdiags.Diagnostics{tfdiags.Sourceless(
tfdiags.Error,
errStateLoadingStateSummary,
fmt.Sprintf(errStateLoadingStateDescription, baseError),
)})
}
func (v *StateJSON) StateSavingError(baseError string) {
v.Diagnostics(tfdiags.Diagnostics{tfdiags.Sourceless(
tfdiags.Error,
errStateRmPersistHeader,
fmt.Sprintf(errStateRmPersistDescription, baseError),
)})
}
func (v *StateJSON) StateListAddr(resAddr addrs.AbsResourceInstance) {
v.view.log.Info(resAddr.String(), "type", "resource_address")
}
func (v *StateJSON) ErrorMovingToAlreadyExistingDst() {
v.Diagnostics(tfdiags.Diagnostics{diagErrStateMvDstExists})
}
func (v *StateJSON) ResourceMoveStatus(dryRun bool, src, dest string) {
if dryRun {
v.view.Info(fmt.Sprintf("Would move %q to %q", src, dest))
return
}
v.view.Info(fmt.Sprintf("Move %q to %q", src, dest))
}
func (v *StateJSON) DryRunMovedStatus(moved int) {
if moved == 0 {
v.view.Info("Would have moved nothing")
}
}
func (v *StateJSON) MoveFinalStatus(moved int) {
if moved == 0 {
v.view.Info("No matching objects found")
return
}
v.view.Info(fmt.Sprintf("Successfully moved %d object(s)", moved))
}
func (v *StateJSON) PrintPulledState(_ string) {
v.view.Error("printing the pulled state is not available in the JSON view. The `tofu state pull` should not be configured with the `-json` flag")
}
func (v *StateJSON) NoMatchingResourcesForProviderReplacement() {
v.view.log.Info("No matching resources found")
}
func (v *StateJSON) ReplaceProviderOverview(from, to addrs.Provider, willReplace []*states.Resource) {
replacedResources := make([]string, len(willReplace))
for i, resource := range willReplace {
replacedResources[i] = resource.Addr.String()
}
msg := fmt.Sprintf("OpenTofu will replace provider from %s to %s for %d resources", from, to, len(willReplace))
v.view.log.Info(msg, "resources", replacedResources, "type", "replace_provider", "from", from.String(), "to", to.String())
}
func (v *StateJSON) ReplaceProviderCancelled() {
v.view.Info("Cancelled replacing providers")
}
func (v *StateJSON) ProviderReplaced(forResources int) {
v.view.Info(fmt.Sprintf("Successfully replaced provider for %d resources", forResources))
}
func (v *StateJSON) ResourceRemoveStatus(dryRun bool, target string) {
if dryRun {
v.view.Info(fmt.Sprintf("Would remove %s", target))
return
}
v.view.Info(fmt.Sprintf("Removed %s", target))
}
func (v *StateJSON) DryRunRemovedStatus(removed int) {
if removed == 0 {
v.view.Info("Would have removed nothing")
}
}
func (v *StateJSON) RemoveFinalStatus(count int) {
if count == 0 {
// NOTE: printing nothing here since this case needs to be handled by the caller
return
}
v.view.Info(fmt.Sprintf("Successfully removed %d resource instance(s)", count))
}
func (v *StateJSON) UnsupportedLocalOp() {
v.Diagnostics(tfdiags.Diagnostics{diagUnsupportedLocalOp})
}
func (v *StateJSON) AddressParsingError(resAddr string) {
v.Diagnostics(tfdiags.Diagnostics{tfdiags.Sourceless(
tfdiags.Error,
fmt.Sprintf(errParsingAddressHeader, resAddr),
errParsingAddressDescription,
)})
}
func (v *StateJSON) NoInstanceFoundError() {
v.Diagnostics(tfdiags.Diagnostics{diagErrNoInstanceFound})
}
func (v *StateJSON) ShowResourceState(_ context.Context, stateFile *statefile.File, schemas *tofu.Schemas) int {
if stateFile == nil {
v.view.Info("no state")
return 0
}
rawState, err := jsonstate.Marshal(stateFile, schemas)
if err != nil {
v.Diagnostics(tfdiags.Diagnostics{}.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to marshal state to json",
fmt.Sprintf("Error while marshalling state to json: %s", err),
)))
return 1
}
_, _ = fmt.Fprintln(v.output, string(rawState))
return 0
}
func (v *StateJSON) Backend() Backend {
return &BackendJSON{
view: v.view,
}
}
var (
diagErrStateNotFound = tfdiags.Sourceless(
tfdiags.Error,
"No state file was found",
`State management commands require a state file. Run this command in a directory where OpenTofu has been run or use the -state flag to point the command to a specific state location.`,
)
diagErrStateMvDstExists = tfdiags.Sourceless(
tfdiags.Error,
"Destination module already exists",
`Please ensure your addresses and state paths are valid. No state was persisted. Your existing states are untouched.`,
)
diagErrNoInstanceFound = tfdiags.Sourceless(
tfdiags.Error,
"No instance found for the given address",
`This command requires that the address references one specific instance. To view the available instances, use "tofu state list". Please modify the address to reference a specific instance.`,
)
)
const (
errStateLoadingStateSummary = "Error loading the state"
errStateLoadingStateDescription = `Please ensure that your OpenTofu state exists and that you've configured it properly. You can use the "-state" flag to point OpenTofu at another state file.
Cause: %s`
)
const (
errStateRmPersistHeader = `Error saving the state`
errStateRmPersistDescription = `The state was not saved. No items were removed from the persisted state. No backup was created since no modification occurred. Please resolve the issue above and try again.
Cause: %s`
)
const (
errParsingAddressHeader = `Error parsing instance address %q`
errParsingAddressDescription = `This command requires that the address references one specific instance. To view the available instances, use "tofu state list". Please modify the address to reference a specific instance.`
)