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