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