github.com/simonswine/terraform@v0.9.0-beta2/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  	opts.Targets = m.targets
   212  	opts.UIInput = m.UIInput()
   213  	opts.Parallelism = m.parallelism
   214  	opts.Shadow = m.shadow
   215  
   216  	return &opts
   217  }
   218  
   219  // flags adds the meta flags to the given FlagSet.
   220  func (m *Meta) flagSet(n string) *flag.FlagSet {
   221  	f := flag.NewFlagSet(n, flag.ContinueOnError)
   222  	f.BoolVar(&m.input, "input", true, "input")
   223  	f.Var((*variables.Flag)(&m.variables), "var", "variables")
   224  	f.Var((*variables.FlagFile)(&m.variables), "var-file", "variable file")
   225  	f.Var((*FlagStringSlice)(&m.targets), "target", "resource to target")
   226  
   227  	if m.autoKey != "" {
   228  		f.Var((*variables.FlagFile)(&m.autoVariables), m.autoKey, "variable file")
   229  	}
   230  
   231  	// Advanced (don't need documentation, or unlikely to be set)
   232  	f.BoolVar(&m.shadow, "shadow", true, "shadow graph")
   233  
   234  	// Experimental features
   235  	experiment.Flag(f)
   236  
   237  	// Create an io.Writer that writes to our Ui properly for errors.
   238  	// This is kind of a hack, but it does the job. Basically: create
   239  	// a pipe, use a scanner to break it into lines, and output each line
   240  	// to the UI. Do this forever.
   241  	errR, errW := io.Pipe()
   242  	errScanner := bufio.NewScanner(errR)
   243  	go func() {
   244  		for errScanner.Scan() {
   245  			m.Ui.Error(errScanner.Text())
   246  		}
   247  	}()
   248  	f.SetOutput(errW)
   249  
   250  	// Set the default Usage to empty
   251  	f.Usage = func() {}
   252  
   253  	return f
   254  }
   255  
   256  // moduleStorage returns the module.Storage implementation used to store
   257  // modules for commands.
   258  func (m *Meta) moduleStorage(root string) getter.Storage {
   259  	return &uiModuleStorage{
   260  		Storage: &getter.FolderStorage{
   261  			StorageDir: filepath.Join(root, "modules"),
   262  		},
   263  		Ui: m.Ui,
   264  	}
   265  }
   266  
   267  // process will process the meta-parameters out of the arguments. This
   268  // will potentially modify the args in-place. It will return the resulting
   269  // slice.
   270  //
   271  // vars says whether or not we support variables.
   272  func (m *Meta) process(args []string, vars bool) []string {
   273  	// We do this so that we retain the ability to technically call
   274  	// process multiple times, even if we have no plans to do so
   275  	if m.oldUi != nil {
   276  		m.Ui = m.oldUi
   277  	}
   278  
   279  	// Set colorization
   280  	m.color = m.Color
   281  	for i, v := range args {
   282  		if v == "-no-color" {
   283  			m.color = false
   284  			m.Color = false
   285  			args = append(args[:i], args[i+1:]...)
   286  			break
   287  		}
   288  	}
   289  
   290  	// Set the UI
   291  	m.oldUi = m.Ui
   292  	m.Ui = &cli.ConcurrentUi{
   293  		Ui: &ColorizeUi{
   294  			Colorize:   m.Colorize(),
   295  			ErrorColor: "[red]",
   296  			WarnColor:  "[yellow]",
   297  			Ui:         m.oldUi,
   298  		},
   299  	}
   300  
   301  	// If we support vars and the default var file exists, add it to
   302  	// the args...
   303  	m.autoKey = ""
   304  	if vars {
   305  		if _, err := os.Stat(DefaultVarsFilename); err == nil {
   306  			m.autoKey = "var-file-default"
   307  			args = append(args, "", "")
   308  			copy(args[2:], args[0:])
   309  			args[0] = "-" + m.autoKey
   310  			args[1] = DefaultVarsFilename
   311  		}
   312  
   313  		if _, err := os.Stat(DefaultVarsFilename + ".json"); err == nil {
   314  			m.autoKey = "var-file-default"
   315  			args = append(args, "", "")
   316  			copy(args[2:], args[0:])
   317  			args[0] = "-" + m.autoKey
   318  			args[1] = DefaultVarsFilename + ".json"
   319  		}
   320  	}
   321  
   322  	return args
   323  }
   324  
   325  // uiHook returns the UiHook to use with the context.
   326  func (m *Meta) uiHook() *UiHook {
   327  	return &UiHook{
   328  		Colorize: m.Colorize(),
   329  		Ui:       m.Ui,
   330  	}
   331  }
   332  
   333  // confirm asks a yes/no confirmation.
   334  func (m *Meta) confirm(opts *terraform.InputOpts) (bool, error) {
   335  	for {
   336  		v, err := m.UIInput().Input(opts)
   337  		if err != nil {
   338  			return false, fmt.Errorf(
   339  				"Error asking for confirmation: %s", err)
   340  		}
   341  
   342  		switch strings.ToLower(v) {
   343  		case "no":
   344  			return false, nil
   345  		case "yes":
   346  			return true, nil
   347  		}
   348  	}
   349  }
   350  
   351  const (
   352  	// ModuleDepthDefault is the default value for
   353  	// module depth, which can be overridden by flag
   354  	// or env var
   355  	ModuleDepthDefault = -1
   356  
   357  	// ModuleDepthEnvVar is the name of the environment variable that can be used to set module depth.
   358  	ModuleDepthEnvVar = "TF_MODULE_DEPTH"
   359  )
   360  
   361  func (m *Meta) addModuleDepthFlag(flags *flag.FlagSet, moduleDepth *int) {
   362  	flags.IntVar(moduleDepth, "module-depth", ModuleDepthDefault, "module-depth")
   363  	if envVar := os.Getenv(ModuleDepthEnvVar); envVar != "" {
   364  		if md, err := strconv.Atoi(envVar); err == nil {
   365  			*moduleDepth = md
   366  		}
   367  	}
   368  }
   369  
   370  // outputShadowError outputs the error from ctx.ShadowError. If the
   371  // error is nil then nothing happens. If output is false then it isn't
   372  // outputted to the user (you can define logic to guard against outputting).
   373  func (m *Meta) outputShadowError(err error, output bool) bool {
   374  	// Do nothing if no error
   375  	if err == nil {
   376  		return false
   377  	}
   378  
   379  	// If not outputting, do nothing
   380  	if !output {
   381  		return false
   382  	}
   383  
   384  	// Write the shadow error output to a file
   385  	path := fmt.Sprintf("terraform-error-%d.log", time.Now().UTC().Unix())
   386  	if err := ioutil.WriteFile(path, []byte(err.Error()), 0644); err != nil {
   387  		// If there is an error writing it, just let it go
   388  		log.Printf("[ERROR] Error writing shadow error: %s", err)
   389  		return false
   390  	}
   391  
   392  	// Output!
   393  	m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
   394  		"[reset][bold][yellow]\nExperimental feature failure! Please report a bug.\n\n"+
   395  			"This is not an error. Your Terraform operation completed successfully.\n"+
   396  			"Your real infrastructure is unaffected by this message.\n\n"+
   397  			"[reset][yellow]While running, Terraform sometimes tests experimental features in the\n"+
   398  			"background. These features cannot affect real state and never touch\n"+
   399  			"real infrastructure. If the features work properly, you see nothing.\n"+
   400  			"If the features fail, this message appears.\n\n"+
   401  			"You can report an issue at: https://github.com/hashicorp/terraform/issues\n\n"+
   402  			"The failure was written to %q. Please\n"+
   403  			"double check this file contains no sensitive information and report\n"+
   404  			"it with your issue.\n\n"+
   405  			"This is not an error. Your terraform operation completed successfully\n"+
   406  			"and your real infrastructure is unaffected by this message.",
   407  		path,
   408  	)))
   409  
   410  	return true
   411  }
   412  
   413  // Env returns the name of the currently configured environment, corresponding
   414  // to the desired named state.
   415  func (m *Meta) Env() string {
   416  	dataDir := m.dataDir
   417  	if m.dataDir == "" {
   418  		dataDir = DefaultDataDir
   419  	}
   420  
   421  	envData, err := ioutil.ReadFile(filepath.Join(dataDir, local.DefaultEnvFile))
   422  	current := string(bytes.TrimSpace(envData))
   423  	if current == "" {
   424  		current = backend.DefaultStateName
   425  	}
   426  
   427  	if err != nil && !os.IsNotExist(err) {
   428  		// always return the default if we can't get an environment name
   429  		log.Printf("[ERROR] failed to read current environment: %s", err)
   430  	}
   431  
   432  	return current
   433  }
   434  
   435  // SetEnv saves the named environment to the local filesystem.
   436  func (m *Meta) SetEnv(name string) error {
   437  	dataDir := m.dataDir
   438  	if m.dataDir == "" {
   439  		dataDir = DefaultDataDir
   440  	}
   441  
   442  	err := os.MkdirAll(dataDir, 0755)
   443  	if err != nil {
   444  		return err
   445  	}
   446  
   447  	err = ioutil.WriteFile(filepath.Join(dataDir, local.DefaultEnvFile), []byte(name), 0644)
   448  	if err != nil {
   449  		return err
   450  	}
   451  	return nil
   452  }