github.com/andrewrynhard/terraform@v0.9.5-0.20170502003928-8d286b83eae4/command/meta.go (about)

     1  package command
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"errors"
     7  	"flag"
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"log"
    12  	"os"
    13  	"path/filepath"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/hashicorp/go-getter"
    19  	"github.com/hashicorp/terraform/backend"
    20  	"github.com/hashicorp/terraform/backend/local"
    21  	"github.com/hashicorp/terraform/helper/experiment"
    22  	"github.com/hashicorp/terraform/helper/variables"
    23  	"github.com/hashicorp/terraform/helper/wrappedstreams"
    24  	"github.com/hashicorp/terraform/terraform"
    25  	"github.com/mitchellh/cli"
    26  	"github.com/mitchellh/colorstring"
    27  )
    28  
    29  // Meta are the meta-options that are available on all or most commands.
    30  type Meta struct {
    31  	// The exported fields below should be set by anyone using a
    32  	// command with a Meta field. These are expected to be set externally
    33  	// (not from within the command itself).
    34  
    35  	Color       bool                   // True if output should be colored
    36  	ContextOpts *terraform.ContextOpts // Opts copied to initialize
    37  	Ui          cli.Ui                 // Ui for output
    38  
    39  	// ExtraHooks are extra hooks to add to the context.
    40  	ExtraHooks []terraform.Hook
    41  
    42  	//----------------------------------------------------------
    43  	// Protected: commands can set these
    44  	//----------------------------------------------------------
    45  
    46  	// Modify the data directory location. Defaults to DefaultDataDir
    47  	dataDir string
    48  
    49  	//----------------------------------------------------------
    50  	// Private: do not set these
    51  	//----------------------------------------------------------
    52  
    53  	// backendState is the currently active backend state
    54  	backendState *terraform.BackendState
    55  
    56  	// Variables for the context (private)
    57  	autoKey       string
    58  	autoVariables map[string]interface{}
    59  	input         bool
    60  	variables     map[string]interface{}
    61  
    62  	// Targets for this context (private)
    63  	targets []string
    64  
    65  	// Internal fields
    66  	color bool
    67  	oldUi cli.Ui
    68  
    69  	// The fields below are expected to be set by the command via
    70  	// command line flags. See the Apply command for an example.
    71  	//
    72  	// statePath is the path to the state file. If this is empty, then
    73  	// no state will be loaded. It is also okay for this to be a path to
    74  	// a file that doesn't exist; it is assumed that this means that there
    75  	// is simply no state.
    76  	//
    77  	// stateOutPath is used to override the output path for the state.
    78  	// If not provided, the StatePath is used causing the old state to
    79  	// be overriden.
    80  	//
    81  	// backupPath is used to backup the state file before writing a modified
    82  	// version. It defaults to stateOutPath + DefaultBackupExtension
    83  	//
    84  	// parallelism is used to control the number of concurrent operations
    85  	// allowed when walking the graph
    86  	//
    87  	// shadow is used to enable/disable the shadow graph
    88  	//
    89  	// provider is to specify specific resource providers
    90  	//
    91  	// stateLock is set to false to disable state locking
    92  	//
    93  	// stateLockTimeout is the optional duration to retry a state locks locks
    94  	// when it is already locked by another process.
    95  	//
    96  	// forceInitCopy suppresses confirmation for copying state data during
    97  	// init.
    98  	//
    99  	// reconfigure forces init to ignore any stored configuration.
   100  	statePath        string
   101  	stateOutPath     string
   102  	backupPath       string
   103  	parallelism      int
   104  	shadow           bool
   105  	provider         string
   106  	stateLock        bool
   107  	stateLockTimeout time.Duration
   108  	forceInitCopy    bool
   109  	reconfigure      bool
   110  }
   111  
   112  // initStatePaths is used to initialize the default values for
   113  // statePath, stateOutPath, and backupPath
   114  func (m *Meta) initStatePaths() {
   115  	if m.statePath == "" {
   116  		m.statePath = DefaultStateFilename
   117  	}
   118  	if m.stateOutPath == "" {
   119  		m.stateOutPath = m.statePath
   120  	}
   121  	if m.backupPath == "" {
   122  		m.backupPath = m.stateOutPath + DefaultBackupExtension
   123  	}
   124  }
   125  
   126  // StateOutPath returns the true output path for the state file
   127  func (m *Meta) StateOutPath() string {
   128  	return m.stateOutPath
   129  }
   130  
   131  // Colorize returns the colorization structure for a command.
   132  func (m *Meta) Colorize() *colorstring.Colorize {
   133  	return &colorstring.Colorize{
   134  		Colors:  colorstring.DefaultColors,
   135  		Disable: !m.color,
   136  		Reset:   true,
   137  	}
   138  }
   139  
   140  // DataDir returns the directory where local data will be stored.
   141  func (m *Meta) DataDir() string {
   142  	dataDir := DefaultDataDir
   143  	if m.dataDir != "" {
   144  		dataDir = m.dataDir
   145  	}
   146  
   147  	return dataDir
   148  }
   149  
   150  const (
   151  	// InputModeEnvVar is the environment variable that, if set to "false" or
   152  	// "0", causes terraform commands to behave as if the `-input=false` flag was
   153  	// specified.
   154  	InputModeEnvVar = "TF_INPUT"
   155  )
   156  
   157  // InputMode returns the type of input we should ask for in the form of
   158  // terraform.InputMode which is passed directly to Context.Input.
   159  func (m *Meta) InputMode() terraform.InputMode {
   160  	if test || !m.input {
   161  		return 0
   162  	}
   163  
   164  	if envVar := os.Getenv(InputModeEnvVar); envVar != "" {
   165  		if v, err := strconv.ParseBool(envVar); err == nil {
   166  			if !v {
   167  				return 0
   168  			}
   169  		}
   170  	}
   171  
   172  	var mode terraform.InputMode
   173  	mode |= terraform.InputModeProvider
   174  	mode |= terraform.InputModeVar
   175  	mode |= terraform.InputModeVarUnset
   176  
   177  	return mode
   178  }
   179  
   180  // UIInput returns a UIInput object to be used for asking for input.
   181  func (m *Meta) UIInput() terraform.UIInput {
   182  	return &UIInput{
   183  		Colorize: m.Colorize(),
   184  	}
   185  }
   186  
   187  // StdinPiped returns true if the input is piped.
   188  func (m *Meta) StdinPiped() bool {
   189  	fi, err := wrappedstreams.Stdin().Stat()
   190  	if err != nil {
   191  		// If there is an error, let's just say its not piped
   192  		return false
   193  	}
   194  
   195  	return fi.Mode()&os.ModeNamedPipe != 0
   196  }
   197  
   198  // contextOpts returns the options to use to initialize a Terraform
   199  // context with the settings from this Meta.
   200  func (m *Meta) contextOpts() *terraform.ContextOpts {
   201  	var opts terraform.ContextOpts
   202  	if v := m.ContextOpts; v != nil {
   203  		opts = *v
   204  	}
   205  
   206  	opts.Hooks = []terraform.Hook{m.uiHook(), &terraform.DebugHook{}}
   207  	if m.ContextOpts != nil {
   208  		opts.Hooks = append(opts.Hooks, m.ContextOpts.Hooks...)
   209  	}
   210  	opts.Hooks = append(opts.Hooks, m.ExtraHooks...)
   211  
   212  	vs := make(map[string]interface{})
   213  	for k, v := range opts.Variables {
   214  		vs[k] = v
   215  	}
   216  	for k, v := range m.autoVariables {
   217  		vs[k] = v
   218  	}
   219  	for k, v := range m.variables {
   220  		vs[k] = v
   221  	}
   222  	opts.Variables = vs
   223  
   224  	opts.Targets = m.targets
   225  	opts.UIInput = m.UIInput()
   226  	opts.Parallelism = m.parallelism
   227  	opts.Shadow = m.shadow
   228  
   229  	opts.Meta = &terraform.ContextMeta{
   230  		Env: m.Env(),
   231  	}
   232  
   233  	return &opts
   234  }
   235  
   236  // flags adds the meta flags to the given FlagSet.
   237  func (m *Meta) flagSet(n string) *flag.FlagSet {
   238  	f := flag.NewFlagSet(n, flag.ContinueOnError)
   239  	f.BoolVar(&m.input, "input", true, "input")
   240  	f.Var((*variables.Flag)(&m.variables), "var", "variables")
   241  	f.Var((*variables.FlagFile)(&m.variables), "var-file", "variable file")
   242  	f.Var((*FlagStringSlice)(&m.targets), "target", "resource to target")
   243  
   244  	if m.autoKey != "" {
   245  		f.Var((*variables.FlagFile)(&m.autoVariables), m.autoKey, "variable file")
   246  	}
   247  
   248  	// Advanced (don't need documentation, or unlikely to be set)
   249  	f.BoolVar(&m.shadow, "shadow", true, "shadow graph")
   250  
   251  	// Experimental features
   252  	experiment.Flag(f)
   253  
   254  	// Create an io.Writer that writes to our Ui properly for errors.
   255  	// This is kind of a hack, but it does the job. Basically: create
   256  	// a pipe, use a scanner to break it into lines, and output each line
   257  	// to the UI. Do this forever.
   258  	errR, errW := io.Pipe()
   259  	errScanner := bufio.NewScanner(errR)
   260  	go func() {
   261  		for errScanner.Scan() {
   262  			m.Ui.Error(errScanner.Text())
   263  		}
   264  	}()
   265  	f.SetOutput(errW)
   266  
   267  	// Set the default Usage to empty
   268  	f.Usage = func() {}
   269  
   270  	// command that bypass locking will supply their own flag on this var, but
   271  	// set the initial meta value to true as a failsafe.
   272  	m.stateLock = true
   273  
   274  	return f
   275  }
   276  
   277  // moduleStorage returns the module.Storage implementation used to store
   278  // modules for commands.
   279  func (m *Meta) moduleStorage(root string) getter.Storage {
   280  	return &uiModuleStorage{
   281  		Storage: &getter.FolderStorage{
   282  			StorageDir: filepath.Join(root, "modules"),
   283  		},
   284  		Ui: m.Ui,
   285  	}
   286  }
   287  
   288  // process will process the meta-parameters out of the arguments. This
   289  // will potentially modify the args in-place. It will return the resulting
   290  // slice.
   291  //
   292  // vars says whether or not we support variables.
   293  func (m *Meta) process(args []string, vars bool) []string {
   294  	// We do this so that we retain the ability to technically call
   295  	// process multiple times, even if we have no plans to do so
   296  	if m.oldUi != nil {
   297  		m.Ui = m.oldUi
   298  	}
   299  
   300  	// Set colorization
   301  	m.color = m.Color
   302  	for i, v := range args {
   303  		if v == "-no-color" {
   304  			m.color = false
   305  			m.Color = false
   306  			args = append(args[:i], args[i+1:]...)
   307  			break
   308  		}
   309  	}
   310  
   311  	// Set the UI
   312  	m.oldUi = m.Ui
   313  	m.Ui = &cli.ConcurrentUi{
   314  		Ui: &ColorizeUi{
   315  			Colorize:   m.Colorize(),
   316  			ErrorColor: "[red]",
   317  			WarnColor:  "[yellow]",
   318  			Ui:         m.oldUi,
   319  		},
   320  	}
   321  
   322  	// If we support vars and the default var file exists, add it to
   323  	// the args...
   324  	m.autoKey = ""
   325  	if vars {
   326  		if _, err := os.Stat(DefaultVarsFilename); err == nil {
   327  			m.autoKey = "var-file-default"
   328  			args = append(args, "", "")
   329  			copy(args[2:], args[0:])
   330  			args[0] = "-" + m.autoKey
   331  			args[1] = DefaultVarsFilename
   332  		}
   333  
   334  		if _, err := os.Stat(DefaultVarsFilename + ".json"); err == nil {
   335  			m.autoKey = "var-file-default"
   336  			args = append(args, "", "")
   337  			copy(args[2:], args[0:])
   338  			args[0] = "-" + m.autoKey
   339  			args[1] = DefaultVarsFilename + ".json"
   340  		}
   341  	}
   342  
   343  	return args
   344  }
   345  
   346  // uiHook returns the UiHook to use with the context.
   347  func (m *Meta) uiHook() *UiHook {
   348  	return &UiHook{
   349  		Colorize: m.Colorize(),
   350  		Ui:       m.Ui,
   351  	}
   352  }
   353  
   354  // confirm asks a yes/no confirmation.
   355  func (m *Meta) confirm(opts *terraform.InputOpts) (bool, error) {
   356  	if !m.input {
   357  		return false, errors.New("input disabled")
   358  	}
   359  	for {
   360  		v, err := m.UIInput().Input(opts)
   361  		if err != nil {
   362  			return false, fmt.Errorf(
   363  				"Error asking for confirmation: %s", err)
   364  		}
   365  
   366  		switch strings.ToLower(v) {
   367  		case "no":
   368  			return false, nil
   369  		case "yes":
   370  			return true, nil
   371  		}
   372  	}
   373  }
   374  
   375  const (
   376  	// ModuleDepthDefault is the default value for
   377  	// module depth, which can be overridden by flag
   378  	// or env var
   379  	ModuleDepthDefault = -1
   380  
   381  	// ModuleDepthEnvVar is the name of the environment variable that can be used to set module depth.
   382  	ModuleDepthEnvVar = "TF_MODULE_DEPTH"
   383  )
   384  
   385  func (m *Meta) addModuleDepthFlag(flags *flag.FlagSet, moduleDepth *int) {
   386  	flags.IntVar(moduleDepth, "module-depth", ModuleDepthDefault, "module-depth")
   387  	if envVar := os.Getenv(ModuleDepthEnvVar); envVar != "" {
   388  		if md, err := strconv.Atoi(envVar); err == nil {
   389  			*moduleDepth = md
   390  		}
   391  	}
   392  }
   393  
   394  // outputShadowError outputs the error from ctx.ShadowError. If the
   395  // error is nil then nothing happens. If output is false then it isn't
   396  // outputted to the user (you can define logic to guard against outputting).
   397  func (m *Meta) outputShadowError(err error, output bool) bool {
   398  	// Do nothing if no error
   399  	if err == nil {
   400  		return false
   401  	}
   402  
   403  	// If not outputting, do nothing
   404  	if !output {
   405  		return false
   406  	}
   407  
   408  	// Write the shadow error output to a file
   409  	path := fmt.Sprintf("terraform-error-%d.log", time.Now().UTC().Unix())
   410  	if err := ioutil.WriteFile(path, []byte(err.Error()), 0644); err != nil {
   411  		// If there is an error writing it, just let it go
   412  		log.Printf("[ERROR] Error writing shadow error: %s", err)
   413  		return false
   414  	}
   415  
   416  	// Output!
   417  	m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
   418  		"[reset][bold][yellow]\nExperimental feature failure! Please report a bug.\n\n"+
   419  			"This is not an error. Your Terraform operation completed successfully.\n"+
   420  			"Your real infrastructure is unaffected by this message.\n\n"+
   421  			"[reset][yellow]While running, Terraform sometimes tests experimental features in the\n"+
   422  			"background. These features cannot affect real state and never touch\n"+
   423  			"real infrastructure. If the features work properly, you see nothing.\n"+
   424  			"If the features fail, this message appears.\n\n"+
   425  			"You can report an issue at: https://github.com/hashicorp/terraform/issues\n\n"+
   426  			"The failure was written to %q. Please\n"+
   427  			"double check this file contains no sensitive information and report\n"+
   428  			"it with your issue.\n\n"+
   429  			"This is not an error. Your terraform operation completed successfully\n"+
   430  			"and your real infrastructure is unaffected by this message.",
   431  		path,
   432  	)))
   433  
   434  	return true
   435  }
   436  
   437  // Env returns the name of the currently configured environment, corresponding
   438  // to the desired named state.
   439  func (m *Meta) Env() string {
   440  	dataDir := m.dataDir
   441  	if m.dataDir == "" {
   442  		dataDir = DefaultDataDir
   443  	}
   444  
   445  	envData, err := ioutil.ReadFile(filepath.Join(dataDir, local.DefaultEnvFile))
   446  	current := string(bytes.TrimSpace(envData))
   447  	if current == "" {
   448  		current = backend.DefaultStateName
   449  	}
   450  
   451  	if err != nil && !os.IsNotExist(err) {
   452  		// always return the default if we can't get an environment name
   453  		log.Printf("[ERROR] failed to read current environment: %s", err)
   454  	}
   455  
   456  	return current
   457  }
   458  
   459  // SetEnv saves the named environment to the local filesystem.
   460  func (m *Meta) SetEnv(name string) error {
   461  	dataDir := m.dataDir
   462  	if m.dataDir == "" {
   463  		dataDir = DefaultDataDir
   464  	}
   465  
   466  	err := os.MkdirAll(dataDir, 0755)
   467  	if err != nil {
   468  		return err
   469  	}
   470  
   471  	err = ioutil.WriteFile(filepath.Join(dataDir, local.DefaultEnvFile), []byte(name), 0644)
   472  	if err != nil {
   473  		return err
   474  	}
   475  	return nil
   476  }