Files
opentf/internal/command/views/workspace.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

383 lines
11 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 (
"bytes"
"fmt"
"github.com/opentofu/opentofu/internal/command/arguments"
"github.com/opentofu/opentofu/internal/tfdiags"
)
type Workspace interface {
Diagnostics(diags tfdiags.Diagnostics)
// General workspace messages
WorkspaceDoesNotExist(name string)
WorkspaceInvalidName(name string)
WorkspaceCreated(name string)
WarnWhenUsedAsEnvCmd(usedAsEnvCmd bool)
// `tofu workspace new` specific
WorkspaceAlreadyExists(name string)
WorkspaceIsOverriddenNewError()
// `tofu workspace list` specific
ListWorkspaces(workspaces []string, current string)
WorkspaceOverwrittenByEnvVarWarn()
// `tofu workspace select` specific
WorkspaceChanged(name string)
WorkspaceIsOverriddenSelectError()
// `tofu workspace delete` specific
WorkspaceDeleted(name string)
DeletedWorkspaceNotEmpty(name string)
CannotDeleteCurrentWorkspace(name string)
// `tofu workspace show` specific
WorkspaceShow(name string)
// Backend returns the non-command view that contains methods to provide
// progress output for the backend operations.
Backend() Backend
}
// NewWorkspace returns an initialized Workspace implementation for the given ViewType.
func NewWorkspace(args arguments.ViewOptions, view *View) Workspace {
var ret Workspace
switch args.ViewType {
case arguments.ViewJSON:
ret = &WorkspaceJSON{view: NewJSONView(view, nil)}
case arguments.ViewHuman:
ret = &WorkspaceHuman{view: view}
default:
panic(fmt.Sprintf("unknown view type %v", args.ViewType))
}
if args.JSONInto != nil {
ret = &WorkspaceMulti{ret, &WorkspaceJSON{view: NewJSONView(view, args.JSONInto)}}
}
return ret
}
type WorkspaceMulti []Workspace
var _ Workspace = (WorkspaceMulti)(nil)
func (m WorkspaceMulti) Diagnostics(diags tfdiags.Diagnostics) {
for _, o := range m {
o.Diagnostics(diags)
}
}
func (m WorkspaceMulti) WorkspaceAlreadyExists(name string) {
for _, o := range m {
o.WorkspaceAlreadyExists(name)
}
}
func (m WorkspaceMulti) WorkspaceDoesNotExist(name string) {
for _, o := range m {
o.WorkspaceDoesNotExist(name)
}
}
func (m WorkspaceMulti) WorkspaceInvalidName(name string) {
for _, o := range m {
o.WorkspaceInvalidName(name)
}
}
func (m WorkspaceMulti) ListWorkspaces(workspaces []string, current string) {
for _, o := range m {
o.ListWorkspaces(workspaces, current)
}
}
func (m WorkspaceMulti) WorkspaceOverwrittenByEnvVarWarn() {
for _, o := range m {
o.WorkspaceOverwrittenByEnvVarWarn()
}
}
func (m WorkspaceMulti) WorkspaceCreated(name string) {
for _, o := range m {
o.WorkspaceCreated(name)
}
}
func (m WorkspaceMulti) WorkspaceChanged(name string) {
for _, o := range m {
o.WorkspaceChanged(name)
}
}
func (m WorkspaceMulti) WorkspaceIsOverriddenSelectError() {
for _, o := range m {
o.WorkspaceIsOverriddenSelectError()
}
}
func (m WorkspaceMulti) WorkspaceIsOverriddenNewError() {
for _, o := range m {
o.WorkspaceIsOverriddenNewError()
}
}
func (m WorkspaceMulti) WorkspaceDeleted(name string) {
for _, o := range m {
o.WorkspaceDeleted(name)
}
}
func (m WorkspaceMulti) DeletedWorkspaceNotEmpty(name string) {
for _, o := range m {
o.DeletedWorkspaceNotEmpty(name)
}
}
func (m WorkspaceMulti) CannotDeleteCurrentWorkspace(name string) {
for _, o := range m {
o.CannotDeleteCurrentWorkspace(name)
}
}
func (m WorkspaceMulti) WorkspaceShow(name string) {
for _, o := range m {
o.WorkspaceShow(name)
}
}
func (m WorkspaceMulti) WarnWhenUsedAsEnvCmd(usedAsEnvCmd bool) {
for _, o := range m {
o.WarnWhenUsedAsEnvCmd(usedAsEnvCmd)
}
}
func (m WorkspaceMulti) Backend() Backend {
ret := make([]Backend, len(m))
for i, v := range m {
ret[i] = v.Backend()
}
return BackendMulti(ret)
}
type WorkspaceHuman struct {
view *View
}
var _ Workspace = (*WorkspaceHuman)(nil)
func (v *WorkspaceHuman) Diagnostics(diags tfdiags.Diagnostics) {
v.view.Diagnostics(diags)
}
func (v *WorkspaceHuman) WorkspaceAlreadyExists(name string) {
v.Diagnostics(tfdiags.Diagnostics{tfdiags.Sourceless(
tfdiags.Error,
fmt.Sprintf("Workspace %q already exists", name),
"A workspace having the given name already exists",
)})
}
func (v *WorkspaceHuman) WorkspaceDoesNotExist(name string) {
v.Diagnostics(tfdiags.Diagnostics{tfdiags.Sourceless(
tfdiags.Error,
fmt.Sprintf("Workspace %q doesn't exist", name),
`You can create this workspace with the "new" subcommand
or include the "-or-create" flag with the "select" subcommand.`,
)})
}
func (v *WorkspaceHuman) WorkspaceInvalidName(name string) {
v.Diagnostics(tfdiags.Diagnostics{tfdiags.Sourceless(
tfdiags.Error,
"Invalid workspace name",
fmt.Sprintf(`The workspace name %q is not allowed. The name must contain only URL safe characters, and no path separators.`, name),
)})
}
func (v *WorkspaceHuman) ListWorkspaces(workspaces []string, current string) {
buf := buildWorkspacesList(workspaces, current)
_, _ = v.view.streams.Println(buf.String())
}
func (v *WorkspaceHuman) WorkspaceOverwrittenByEnvVarWarn() {
msg := `
The active workspace is being overridden using the TF_WORKSPACE environment
variable.
`
_, _ = v.view.streams.Println(msg)
}
func (v *WorkspaceHuman) WorkspaceCreated(name string) {
const msg = `[reset][green][bold]Created and switched to workspace %q![reset][green]
You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "tofu plan" OpenTofu will not see any existing state
for this configuration.`
colorisedMsg := fmt.Sprintf(v.view.colorize.Color(msg), name)
_, _ = v.view.streams.Println(colorisedMsg)
}
func (v *WorkspaceHuman) WorkspaceChanged(name string) {
const msg = `[reset][green]Switched to workspace %q.`
colorisedMsg := fmt.Sprintf(v.view.colorize.Color(msg), name)
_, _ = v.view.streams.Println(colorisedMsg)
}
func (v *WorkspaceHuman) WorkspaceIsOverriddenSelectError() {
v.Diagnostics(tfdiags.Diagnostics{tfdiags.Sourceless(
tfdiags.Error,
"The selected workspace is overridden using the TF_WORKSPACE environment variable",
`To select a new workspace, either update this environment variable or unset it and then run this command again.`,
)})
}
func (v *WorkspaceHuman) WorkspaceIsOverriddenNewError() {
v.Diagnostics(tfdiags.Diagnostics{tfdiags.Sourceless(
tfdiags.Error,
"The workspace is overridden using the TF_WORKSPACE environment variable",
`To create a new workspace, either unset this environment variable or update it to match the workspace name you are trying to create, and then run this command again.`,
)})
}
func (v *WorkspaceHuman) WorkspaceDeleted(name string) {
const msg = `[reset][green]Deleted workspace %q!`
colorisedMsg := fmt.Sprintf(v.view.colorize.Color(msg), name)
_, _ = v.view.streams.Println(colorisedMsg)
}
func (v *WorkspaceHuman) DeletedWorkspaceNotEmpty(name string) {
v.Diagnostics(tfdiags.Diagnostics{tfdiags.Sourceless(
tfdiags.Warning,
fmt.Sprintf("%q was non-empty", name),
`The resources managed by the deleted workspace may still exist, but are no longer manageable by OpenTofu since the state has been deleted.`,
)})
}
func (v *WorkspaceHuman) CannotDeleteCurrentWorkspace(name string) {
v.Diagnostics(tfdiags.Diagnostics{tfdiags.Sourceless(
tfdiags.Error,
fmt.Sprintf("Workspace %[1]q is your active workspace", name),
`You cannot delete the currently active workspace. Please switch to another workspace and try again.`,
)})
}
func (v *WorkspaceHuman) WorkspaceShow(name string) {
_, _ = v.view.streams.Println(name)
}
func (v *WorkspaceHuman) WarnWhenUsedAsEnvCmd(usedAsEnvCmd bool) {
if !usedAsEnvCmd {
return
}
v.Diagnostics(tfdiags.Diagnostics{tfdiags.Sourceless(
tfdiags.Warning,
`The "tofu env" family of commands is deprecated`,
`"Workspace" is now the preferred term for what earlier OpenTofu versions called "environment", to reduce ambiguity caused by the latter term colliding with other concepts.
The "tofu workspace" commands should be used instead. "tofu env" will be removed in a future OpenTofu version.`,
)})
}
func (v *WorkspaceHuman) Backend() Backend {
return &BackendHuman{
view: v.view,
}
}
type WorkspaceJSON struct {
view *JSONView
}
var _ Workspace = (*WorkspaceJSON)(nil)
func (v *WorkspaceJSON) Diagnostics(diags tfdiags.Diagnostics) {
v.view.Diagnostics(diags)
}
func (v *WorkspaceJSON) WorkspaceAlreadyExists(name string) {
v.view.Error(fmt.Sprintf("Workspace %q already exists", name))
}
func (v *WorkspaceJSON) WorkspaceDoesNotExist(name string) {
v.view.Error(fmt.Sprintf("Workspace %q doesn't exist. You can create this workspace with the \"new\" subcommand or include the \"-or-create\" flag with the \"select\" subcommand", name))
}
func (v *WorkspaceJSON) WorkspaceInvalidName(name string) {
v.view.Error(fmt.Sprintf("The workspace name %q is not allowed. The name must contain only URL safe characters, and no path separators", name))
}
func (v *WorkspaceJSON) ListWorkspaces(workspaces []string, current string) {
v.view.log.Info("list of workspaces", "type", "workspace_list", "workspaces", workspaces, "current_workspace", current)
}
func (v *WorkspaceJSON) WorkspaceOverwrittenByEnvVarWarn() {
v.view.Info("The active workspace is being overridden using the TF_WORKSPACE environment variable")
}
func (v *WorkspaceJSON) WorkspaceCreated(name string) {
v.view.Info(fmt.Sprintf("Created and switched to workspace %q. You're now on a new, empty workspace. Workspaces isolate their state, so if you run \"tofu plan\" OpenTofu will not see any existing state for this configuration", name))
}
func (v *WorkspaceJSON) WorkspaceChanged(name string) {
v.view.Info(fmt.Sprintf("Switched to workspace %q", name))
}
func (v *WorkspaceJSON) WorkspaceIsOverriddenSelectError() {
v.view.Error("The selected workspace is currently overridden using the TF_WORKSPACE environment variable. To select a new workspace, either update this environment variable or unset it and then run this command again")
}
func (v *WorkspaceJSON) WorkspaceIsOverriddenNewError() {
v.view.Error("The workspace is currently overridden using the TF_WORKSPACE environment variable. You cannot create a new workspace when using this setting. To create a new workspace, either unset this environment variable or update it to match the workspace name you are trying to create, and then run this command again")
}
func (v *WorkspaceJSON) WorkspaceDeleted(name string) {
v.view.Info(fmt.Sprintf("Deleted workspace %q", name))
}
func (v *WorkspaceJSON) DeletedWorkspaceNotEmpty(name string) {
v.view.Warn(fmt.Sprintf("%q was non-empty. The resources managed by the deleted workspace may still exist, but are no longer manageable by OpenTofu since the state has been deleted", name))
}
func (v *WorkspaceJSON) CannotDeleteCurrentWorkspace(name string) {
v.view.Error(fmt.Sprintf("Workspace %q is your active workspace. You cannot delete the currently active workspace. Please switch to another workspace and try again", name))
}
func (v *WorkspaceJSON) WorkspaceShow(name string) {
v.view.Info(name)
}
func (v *WorkspaceJSON) WarnWhenUsedAsEnvCmd(usedAsEnvCmd bool) {
if !usedAsEnvCmd {
return
}
v.view.Warn("The \"tofu env\" family of commands is deprecated. Use \"tofu workspace\" instead")
}
func (v *WorkspaceJSON) Backend() Backend {
return &BackendJSON{
view: v.view,
}
}
func buildWorkspacesList(workspaces []string, current string) bytes.Buffer {
var ret bytes.Buffer
for _, s := range workspaces {
if s == current {
ret.WriteString("* ")
} else {
ret.WriteString(" ")
}
ret.WriteString(s + "\n")
}
return ret
}