mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 18:01:01 -06:00
418a8a8bc9
We're shifting terminology from "environment" to "workspace". This takes care of some of the main internal API surface that was using the old terminology, though is not intended to be entirely comprehensive and is mainly just to minimize the amount of confusion for maintainers as we continue moving towards eliminating the old terminology.
518 lines
14 KiB
Go
518 lines
14 KiB
Go
package command
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-getter"
|
|
"github.com/hashicorp/terraform/backend"
|
|
"github.com/hashicorp/terraform/backend/local"
|
|
"github.com/hashicorp/terraform/helper/experiment"
|
|
"github.com/hashicorp/terraform/helper/variables"
|
|
"github.com/hashicorp/terraform/helper/wrappedstreams"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
"github.com/mitchellh/cli"
|
|
"github.com/mitchellh/colorstring"
|
|
)
|
|
|
|
// Meta are the meta-options that are available on all or most commands.
|
|
type Meta struct {
|
|
// The exported fields below should be set by anyone using a
|
|
// command with a Meta field. These are expected to be set externally
|
|
// (not from within the command itself).
|
|
|
|
Color bool // True if output should be colored
|
|
GlobalPluginDirs []string // Additional paths to search for plugins
|
|
PluginOverrides *PluginOverrides // legacy overrides from .terraformrc file
|
|
Ui cli.Ui // Ui for output
|
|
|
|
// ExtraHooks are extra hooks to add to the context.
|
|
ExtraHooks []terraform.Hook
|
|
|
|
//----------------------------------------------------------
|
|
// Protected: commands can set these
|
|
//----------------------------------------------------------
|
|
|
|
// Modify the data directory location. Defaults to DefaultDataDir
|
|
dataDir string
|
|
|
|
// Override certain behavior for tests within this package
|
|
testingOverrides *testingOverrides
|
|
|
|
//----------------------------------------------------------
|
|
// Private: do not set these
|
|
//----------------------------------------------------------
|
|
|
|
// backendState is the currently active backend state
|
|
backendState *terraform.BackendState
|
|
|
|
// Variables for the context (private)
|
|
autoKey string
|
|
autoVariables map[string]interface{}
|
|
input bool
|
|
variables map[string]interface{}
|
|
|
|
// Targets for this context (private)
|
|
targets []string
|
|
|
|
// Internal fields
|
|
color bool
|
|
oldUi cli.Ui
|
|
|
|
// The fields below are expected to be set by the command via
|
|
// command line flags. See the Apply command for an example.
|
|
//
|
|
// statePath is the path to the state file. If this is empty, then
|
|
// no state will be loaded. It is also okay for this to be a path to
|
|
// a file that doesn't exist; it is assumed that this means that there
|
|
// is simply no state.
|
|
//
|
|
// stateOutPath is used to override the output path for the state.
|
|
// If not provided, the StatePath is used causing the old state to
|
|
// be overriden.
|
|
//
|
|
// backupPath is used to backup the state file before writing a modified
|
|
// version. It defaults to stateOutPath + DefaultBackupExtension
|
|
//
|
|
// parallelism is used to control the number of concurrent operations
|
|
// allowed when walking the graph
|
|
//
|
|
// shadow is used to enable/disable the shadow graph
|
|
//
|
|
// provider is to specify specific resource providers
|
|
//
|
|
// stateLock is set to false to disable state locking
|
|
//
|
|
// stateLockTimeout is the optional duration to retry a state locks locks
|
|
// when it is already locked by another process.
|
|
//
|
|
// forceInitCopy suppresses confirmation for copying state data during
|
|
// init.
|
|
//
|
|
// reconfigure forces init to ignore any stored configuration.
|
|
statePath string
|
|
stateOutPath string
|
|
backupPath string
|
|
parallelism int
|
|
shadow bool
|
|
provider string
|
|
stateLock bool
|
|
stateLockTimeout time.Duration
|
|
forceInitCopy bool
|
|
reconfigure bool
|
|
}
|
|
|
|
type PluginOverrides struct {
|
|
Providers map[string]string
|
|
Provisioners map[string]string
|
|
}
|
|
|
|
type testingOverrides struct {
|
|
ProviderResolver terraform.ResourceProviderResolver
|
|
Provisioners map[string]terraform.ResourceProvisionerFactory
|
|
}
|
|
|
|
// initStatePaths is used to initialize the default values for
|
|
// statePath, stateOutPath, and backupPath
|
|
func (m *Meta) initStatePaths() {
|
|
if m.statePath == "" {
|
|
m.statePath = DefaultStateFilename
|
|
}
|
|
if m.stateOutPath == "" {
|
|
m.stateOutPath = m.statePath
|
|
}
|
|
if m.backupPath == "" {
|
|
m.backupPath = m.stateOutPath + DefaultBackupExtension
|
|
}
|
|
}
|
|
|
|
// StateOutPath returns the true output path for the state file
|
|
func (m *Meta) StateOutPath() string {
|
|
return m.stateOutPath
|
|
}
|
|
|
|
// Colorize returns the colorization structure for a command.
|
|
func (m *Meta) Colorize() *colorstring.Colorize {
|
|
return &colorstring.Colorize{
|
|
Colors: colorstring.DefaultColors,
|
|
Disable: !m.color,
|
|
Reset: true,
|
|
}
|
|
}
|
|
|
|
// DataDir returns the directory where local data will be stored.
|
|
func (m *Meta) DataDir() string {
|
|
dataDir := DefaultDataDir
|
|
if m.dataDir != "" {
|
|
dataDir = m.dataDir
|
|
}
|
|
|
|
return dataDir
|
|
}
|
|
|
|
const (
|
|
// InputModeEnvVar is the environment variable that, if set to "false" or
|
|
// "0", causes terraform commands to behave as if the `-input=false` flag was
|
|
// specified.
|
|
InputModeEnvVar = "TF_INPUT"
|
|
)
|
|
|
|
// InputMode returns the type of input we should ask for in the form of
|
|
// terraform.InputMode which is passed directly to Context.Input.
|
|
func (m *Meta) InputMode() terraform.InputMode {
|
|
if test || !m.input {
|
|
return 0
|
|
}
|
|
|
|
if envVar := os.Getenv(InputModeEnvVar); envVar != "" {
|
|
if v, err := strconv.ParseBool(envVar); err == nil {
|
|
if !v {
|
|
return 0
|
|
}
|
|
}
|
|
}
|
|
|
|
var mode terraform.InputMode
|
|
mode |= terraform.InputModeProvider
|
|
mode |= terraform.InputModeVar
|
|
mode |= terraform.InputModeVarUnset
|
|
|
|
return mode
|
|
}
|
|
|
|
// UIInput returns a UIInput object to be used for asking for input.
|
|
func (m *Meta) UIInput() terraform.UIInput {
|
|
return &UIInput{
|
|
Colorize: m.Colorize(),
|
|
}
|
|
}
|
|
|
|
// StdinPiped returns true if the input is piped.
|
|
func (m *Meta) StdinPiped() bool {
|
|
fi, err := wrappedstreams.Stdin().Stat()
|
|
if err != nil {
|
|
// If there is an error, let's just say its not piped
|
|
return false
|
|
}
|
|
|
|
return fi.Mode()&os.ModeNamedPipe != 0
|
|
}
|
|
|
|
// contextOpts returns the options to use to initialize a Terraform
|
|
// context with the settings from this Meta.
|
|
func (m *Meta) contextOpts() *terraform.ContextOpts {
|
|
var opts terraform.ContextOpts
|
|
opts.Hooks = []terraform.Hook{m.uiHook(), &terraform.DebugHook{}}
|
|
opts.Hooks = append(opts.Hooks, m.ExtraHooks...)
|
|
|
|
vs := make(map[string]interface{})
|
|
for k, v := range opts.Variables {
|
|
vs[k] = v
|
|
}
|
|
for k, v := range m.autoVariables {
|
|
vs[k] = v
|
|
}
|
|
for k, v := range m.variables {
|
|
vs[k] = v
|
|
}
|
|
opts.Variables = vs
|
|
|
|
opts.Targets = m.targets
|
|
opts.UIInput = m.UIInput()
|
|
opts.Parallelism = m.parallelism
|
|
opts.Shadow = m.shadow
|
|
|
|
// If testingOverrides are set, we'll skip the plugin discovery process
|
|
// and just work with what we've been given, thus allowing the tests
|
|
// to provide mock providers and provisioners.
|
|
if m.testingOverrides != nil {
|
|
opts.ProviderResolver = m.testingOverrides.ProviderResolver
|
|
opts.Provisioners = m.testingOverrides.Provisioners
|
|
} else {
|
|
opts.ProviderResolver = m.providerResolver()
|
|
opts.Provisioners = m.provisionerFactories()
|
|
}
|
|
|
|
opts.ProviderSHA256s = m.providerPluginsLock().Read()
|
|
|
|
opts.Meta = &terraform.ContextMeta{
|
|
Env: m.Workspace(),
|
|
}
|
|
|
|
return &opts
|
|
}
|
|
|
|
// flags adds the meta flags to the given FlagSet.
|
|
func (m *Meta) flagSet(n string) *flag.FlagSet {
|
|
f := flag.NewFlagSet(n, flag.ContinueOnError)
|
|
f.BoolVar(&m.input, "input", true, "input")
|
|
f.Var((*variables.Flag)(&m.variables), "var", "variables")
|
|
f.Var((*variables.FlagFile)(&m.variables), "var-file", "variable file")
|
|
f.Var((*FlagStringSlice)(&m.targets), "target", "resource to target")
|
|
|
|
if m.autoKey != "" {
|
|
f.Var((*variables.FlagFile)(&m.autoVariables), m.autoKey, "variable file")
|
|
}
|
|
|
|
// Advanced (don't need documentation, or unlikely to be set)
|
|
f.BoolVar(&m.shadow, "shadow", true, "shadow graph")
|
|
|
|
// Experimental features
|
|
experiment.Flag(f)
|
|
|
|
// Create an io.Writer that writes to our Ui properly for errors.
|
|
// This is kind of a hack, but it does the job. Basically: create
|
|
// a pipe, use a scanner to break it into lines, and output each line
|
|
// to the UI. Do this forever.
|
|
errR, errW := io.Pipe()
|
|
errScanner := bufio.NewScanner(errR)
|
|
go func() {
|
|
for errScanner.Scan() {
|
|
m.Ui.Error(errScanner.Text())
|
|
}
|
|
}()
|
|
f.SetOutput(errW)
|
|
|
|
// Set the default Usage to empty
|
|
f.Usage = func() {}
|
|
|
|
// command that bypass locking will supply their own flag on this var, but
|
|
// set the initial meta value to true as a failsafe.
|
|
m.stateLock = true
|
|
|
|
return f
|
|
}
|
|
|
|
// moduleStorage returns the module.Storage implementation used to store
|
|
// modules for commands.
|
|
func (m *Meta) moduleStorage(root string) getter.Storage {
|
|
return &uiModuleStorage{
|
|
Storage: &getter.FolderStorage{
|
|
StorageDir: filepath.Join(root, "modules"),
|
|
},
|
|
Ui: m.Ui,
|
|
}
|
|
}
|
|
|
|
// process will process the meta-parameters out of the arguments. This
|
|
// will potentially modify the args in-place. It will return the resulting
|
|
// slice.
|
|
//
|
|
// vars says whether or not we support variables.
|
|
func (m *Meta) process(args []string, vars bool) []string {
|
|
// We do this so that we retain the ability to technically call
|
|
// process multiple times, even if we have no plans to do so
|
|
if m.oldUi != nil {
|
|
m.Ui = m.oldUi
|
|
}
|
|
|
|
// Set colorization
|
|
m.color = m.Color
|
|
for i, v := range args {
|
|
if v == "-no-color" {
|
|
m.color = false
|
|
m.Color = false
|
|
args = append(args[:i], args[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
// Set the UI
|
|
m.oldUi = m.Ui
|
|
m.Ui = &cli.ConcurrentUi{
|
|
Ui: &ColorizeUi{
|
|
Colorize: m.Colorize(),
|
|
ErrorColor: "[red]",
|
|
WarnColor: "[yellow]",
|
|
Ui: m.oldUi,
|
|
},
|
|
}
|
|
|
|
// If we support vars and the default var file exists, add it to
|
|
// the args...
|
|
m.autoKey = ""
|
|
if vars {
|
|
if _, err := os.Stat(DefaultVarsFilename); err == nil {
|
|
m.autoKey = "var-file-default"
|
|
args = append(args, "", "")
|
|
copy(args[2:], args[0:])
|
|
args[0] = "-" + m.autoKey
|
|
args[1] = DefaultVarsFilename
|
|
}
|
|
|
|
if _, err := os.Stat(DefaultVarsFilename + ".json"); err == nil {
|
|
m.autoKey = "var-file-default"
|
|
args = append(args, "", "")
|
|
copy(args[2:], args[0:])
|
|
args[0] = "-" + m.autoKey
|
|
args[1] = DefaultVarsFilename + ".json"
|
|
}
|
|
}
|
|
|
|
return args
|
|
}
|
|
|
|
// uiHook returns the UiHook to use with the context.
|
|
func (m *Meta) uiHook() *UiHook {
|
|
return &UiHook{
|
|
Colorize: m.Colorize(),
|
|
Ui: m.Ui,
|
|
}
|
|
}
|
|
|
|
// confirm asks a yes/no confirmation.
|
|
func (m *Meta) confirm(opts *terraform.InputOpts) (bool, error) {
|
|
if !m.input {
|
|
return false, errors.New("input disabled")
|
|
}
|
|
for {
|
|
v, err := m.UIInput().Input(opts)
|
|
if err != nil {
|
|
return false, fmt.Errorf(
|
|
"Error asking for confirmation: %s", err)
|
|
}
|
|
|
|
switch strings.ToLower(v) {
|
|
case "no":
|
|
return false, nil
|
|
case "yes":
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
const (
|
|
// ModuleDepthDefault is the default value for
|
|
// module depth, which can be overridden by flag
|
|
// or env var
|
|
ModuleDepthDefault = -1
|
|
|
|
// ModuleDepthEnvVar is the name of the environment variable that can be used to set module depth.
|
|
ModuleDepthEnvVar = "TF_MODULE_DEPTH"
|
|
)
|
|
|
|
func (m *Meta) addModuleDepthFlag(flags *flag.FlagSet, moduleDepth *int) {
|
|
flags.IntVar(moduleDepth, "module-depth", ModuleDepthDefault, "module-depth")
|
|
if envVar := os.Getenv(ModuleDepthEnvVar); envVar != "" {
|
|
if md, err := strconv.Atoi(envVar); err == nil {
|
|
*moduleDepth = md
|
|
}
|
|
}
|
|
}
|
|
|
|
// outputShadowError outputs the error from ctx.ShadowError. If the
|
|
// error is nil then nothing happens. If output is false then it isn't
|
|
// outputted to the user (you can define logic to guard against outputting).
|
|
func (m *Meta) outputShadowError(err error, output bool) bool {
|
|
// Do nothing if no error
|
|
if err == nil {
|
|
return false
|
|
}
|
|
|
|
// If not outputting, do nothing
|
|
if !output {
|
|
return false
|
|
}
|
|
|
|
// Write the shadow error output to a file
|
|
path := fmt.Sprintf("terraform-error-%d.log", time.Now().UTC().Unix())
|
|
if err := ioutil.WriteFile(path, []byte(err.Error()), 0644); err != nil {
|
|
// If there is an error writing it, just let it go
|
|
log.Printf("[ERROR] Error writing shadow error: %s", err)
|
|
return false
|
|
}
|
|
|
|
// Output!
|
|
m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
|
|
"[reset][bold][yellow]\nExperimental feature failure! Please report a bug.\n\n"+
|
|
"This is not an error. Your Terraform operation completed successfully.\n"+
|
|
"Your real infrastructure is unaffected by this message.\n\n"+
|
|
"[reset][yellow]While running, Terraform sometimes tests experimental features in the\n"+
|
|
"background. These features cannot affect real state and never touch\n"+
|
|
"real infrastructure. If the features work properly, you see nothing.\n"+
|
|
"If the features fail, this message appears.\n\n"+
|
|
"You can report an issue at: https://github.com/hashicorp/terraform/issues\n\n"+
|
|
"The failure was written to %q. Please\n"+
|
|
"double check this file contains no sensitive information and report\n"+
|
|
"it with your issue.\n\n"+
|
|
"This is not an error. Your terraform operation completed successfully\n"+
|
|
"and your real infrastructure is unaffected by this message.",
|
|
path,
|
|
)))
|
|
|
|
return true
|
|
}
|
|
|
|
// WorkspaceNameEnvVar is the name of the environment variable that can be used
|
|
// to set the name of the Terraform workspace, overriding the workspace chosen
|
|
// by `terraform workspace select`.
|
|
//
|
|
// Note that this environment variable is ignored by `terraform workspace new`
|
|
// and `terraform workspace delete`.
|
|
const WorkspaceNameEnvVar = "TF_WORKSPACE"
|
|
|
|
// Workspace returns the name of the currently configured workspace, corresponding
|
|
// to the desired named state.
|
|
func (m *Meta) Workspace() string {
|
|
current, _ := m.WorkspaceOverridden()
|
|
return current
|
|
}
|
|
|
|
// WorkspaceOverridden returns the name of the currently configured workspace,
|
|
// corresponding to the desired named state, as well as a bool saying whether
|
|
// this was set via the TF_WORKSPACE environment variable.
|
|
func (m *Meta) WorkspaceOverridden() (string, bool) {
|
|
if envVar := os.Getenv(WorkspaceNameEnvVar); envVar != "" {
|
|
return envVar, true
|
|
}
|
|
|
|
dataDir := m.dataDir
|
|
if m.dataDir == "" {
|
|
dataDir = DefaultDataDir
|
|
}
|
|
|
|
envData, err := ioutil.ReadFile(filepath.Join(dataDir, local.DefaultWorkspaceFile))
|
|
current := string(bytes.TrimSpace(envData))
|
|
if current == "" {
|
|
current = backend.DefaultStateName
|
|
}
|
|
|
|
if err != nil && !os.IsNotExist(err) {
|
|
// always return the default if we can't get a workspace name
|
|
log.Printf("[ERROR] failed to read current workspace: %s", err)
|
|
}
|
|
|
|
return current, false
|
|
}
|
|
|
|
// SetWorkspace saves the given name as the current workspace in the local
|
|
// filesystem.
|
|
func (m *Meta) SetWorkspace(name string) error {
|
|
dataDir := m.dataDir
|
|
if m.dataDir == "" {
|
|
dataDir = DefaultDataDir
|
|
}
|
|
|
|
err := os.MkdirAll(dataDir, 0755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = ioutil.WriteFile(filepath.Join(dataDir, local.DefaultWorkspaceFile), []byte(name), 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|