github.com/peterbale/terraform@v0.9.0-beta2.0.20170315142748-5723acd55547/command/meta.go (about)

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