mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-05-17 01:03:30 -04:00
578 lines
16 KiB
Go
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.`
|
|
)
|