github.com/ggriffiths/terraform@v0.9.0-beta1.0.20170222213024-79c4935604cb/command/meta.go (about)

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