github.com/hooklift/terraform@v0.11.0-beta1.0.20171117000744-6786c1361ffe/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  	"sort"
    15  	"strconv"
    16  	"strings"
    17  	"time"
    18  
    19  	"github.com/hashicorp/terraform/backend"
    20  	"github.com/hashicorp/terraform/backend/local"
    21  	"github.com/hashicorp/terraform/command/format"
    22  	"github.com/hashicorp/terraform/config"
    23  	"github.com/hashicorp/terraform/config/module"
    24  	"github.com/hashicorp/terraform/helper/experiment"
    25  	"github.com/hashicorp/terraform/helper/variables"
    26  	"github.com/hashicorp/terraform/helper/wrappedstreams"
    27  	"github.com/hashicorp/terraform/svchost/auth"
    28  	"github.com/hashicorp/terraform/svchost/disco"
    29  	"github.com/hashicorp/terraform/terraform"
    30  	"github.com/hashicorp/terraform/tfdiags"
    31  	"github.com/mitchellh/cli"
    32  	"github.com/mitchellh/colorstring"
    33  )
    34  
    35  // Meta are the meta-options that are available on all or most commands.
    36  type Meta struct {
    37  	// The exported fields below should be set by anyone using a
    38  	// command with a Meta field. These are expected to be set externally
    39  	// (not from within the command itself).
    40  
    41  	Color            bool             // True if output should be colored
    42  	GlobalPluginDirs []string         // Additional paths to search for plugins
    43  	PluginOverrides  *PluginOverrides // legacy overrides from .terraformrc file
    44  	Ui               cli.Ui           // Ui for output
    45  
    46  	// ExtraHooks are extra hooks to add to the context.
    47  	ExtraHooks []terraform.Hook
    48  
    49  	// Services provides access to remote endpoint information for
    50  	// "terraform-native' services running at a specific user-facing hostname.
    51  	Services *disco.Disco
    52  
    53  	// Credentials provides access to credentials for "terraform-native"
    54  	// services, which are accessed by a service hostname.
    55  	Credentials auth.CredentialsSource
    56  
    57  	// RunningInAutomation indicates that commands are being run by an
    58  	// automated system rather than directly at a command prompt.
    59  	//
    60  	// This is a hint to various command routines that it may be confusing
    61  	// to print out messages that suggest running specific follow-up
    62  	// commands, since the user consuming the output will not be
    63  	// in a position to run such commands.
    64  	//
    65  	// The intended use-case of this flag is when Terraform is running in
    66  	// some sort of workflow orchestration tool which is abstracting away
    67  	// the specific commands being run.
    68  	RunningInAutomation bool
    69  
    70  	// PluginCacheDir, if non-empty, enables caching of downloaded plugins
    71  	// into the given directory.
    72  	PluginCacheDir string
    73  
    74  	// OverrideDataDir, if non-empty, overrides the return value of the
    75  	// DataDir method for situations where the local .terraform/ directory
    76  	// is not suitable, e.g. because of a read-only filesystem.
    77  	OverrideDataDir string
    78  
    79  	//----------------------------------------------------------
    80  	// Protected: commands can set these
    81  	//----------------------------------------------------------
    82  
    83  	// Modify the data directory location. This should be accessed through the
    84  	// DataDir method.
    85  	dataDir string
    86  
    87  	// pluginPath is a user defined set of directories to look for plugins.
    88  	// This is set during init with the `-plugin-dir` flag, saved to a file in
    89  	// the data directory.
    90  	// This overrides all other search paths when discovering plugins.
    91  	pluginPath []string
    92  
    93  	ignorePluginChecksum bool
    94  
    95  	// Override certain behavior for tests within this package
    96  	testingOverrides *testingOverrides
    97  
    98  	//----------------------------------------------------------
    99  	// Private: do not set these
   100  	//----------------------------------------------------------
   101  
   102  	// backendState is the currently active backend state
   103  	backendState *terraform.BackendState
   104  
   105  	// Variables for the context (private)
   106  	autoKey       string
   107  	autoVariables map[string]interface{}
   108  	input         bool
   109  	variables     map[string]interface{}
   110  
   111  	// Targets for this context (private)
   112  	targets []string
   113  
   114  	// Internal fields
   115  	color bool
   116  	oldUi cli.Ui
   117  
   118  	// The fields below are expected to be set by the command via
   119  	// command line flags. See the Apply command for an example.
   120  	//
   121  	// statePath is the path to the state file. If this is empty, then
   122  	// no state will be loaded. It is also okay for this to be a path to
   123  	// a file that doesn't exist; it is assumed that this means that there
   124  	// is simply no state.
   125  	//
   126  	// stateOutPath is used to override the output path for the state.
   127  	// If not provided, the StatePath is used causing the old state to
   128  	// be overriden.
   129  	//
   130  	// backupPath is used to backup the state file before writing a modified
   131  	// version. It defaults to stateOutPath + DefaultBackupExtension
   132  	//
   133  	// parallelism is used to control the number of concurrent operations
   134  	// allowed when walking the graph
   135  	//
   136  	// shadow is used to enable/disable the shadow graph
   137  	//
   138  	// provider is to specify specific resource providers
   139  	//
   140  	// stateLock is set to false to disable state locking
   141  	//
   142  	// stateLockTimeout is the optional duration to retry a state locks locks
   143  	// when it is already locked by another process.
   144  	//
   145  	// forceInitCopy suppresses confirmation for copying state data during
   146  	// init.
   147  	//
   148  	// reconfigure forces init to ignore any stored configuration.
   149  	statePath        string
   150  	stateOutPath     string
   151  	backupPath       string
   152  	parallelism      int
   153  	shadow           bool
   154  	provider         string
   155  	stateLock        bool
   156  	stateLockTimeout time.Duration
   157  	forceInitCopy    bool
   158  	reconfigure      bool
   159  
   160  	// errWriter is the write side of a pipe for the FlagSet output. We need to
   161  	// keep track of this to close previous pipes between tests. Normal
   162  	// operation never needs to close this.
   163  	errWriter *io.PipeWriter
   164  	// done chan to wait for the scanner goroutine
   165  	errScannerDone chan struct{}
   166  
   167  	// Used with the import command to allow import of state when no matching config exists.
   168  	allowMissingConfig bool
   169  }
   170  
   171  type PluginOverrides struct {
   172  	Providers    map[string]string
   173  	Provisioners map[string]string
   174  }
   175  
   176  type testingOverrides struct {
   177  	ProviderResolver terraform.ResourceProviderResolver
   178  	Provisioners     map[string]terraform.ResourceProvisionerFactory
   179  }
   180  
   181  // initStatePaths is used to initialize the default values for
   182  // statePath, stateOutPath, and backupPath
   183  func (m *Meta) initStatePaths() {
   184  	if m.statePath == "" {
   185  		m.statePath = DefaultStateFilename
   186  	}
   187  	if m.stateOutPath == "" {
   188  		m.stateOutPath = m.statePath
   189  	}
   190  	if m.backupPath == "" {
   191  		m.backupPath = m.stateOutPath + DefaultBackupExtension
   192  	}
   193  }
   194  
   195  // StateOutPath returns the true output path for the state file
   196  func (m *Meta) StateOutPath() string {
   197  	return m.stateOutPath
   198  }
   199  
   200  // Colorize returns the colorization structure for a command.
   201  func (m *Meta) Colorize() *colorstring.Colorize {
   202  	return &colorstring.Colorize{
   203  		Colors:  colorstring.DefaultColors,
   204  		Disable: !m.color,
   205  		Reset:   true,
   206  	}
   207  }
   208  
   209  // DataDir returns the directory where local data will be stored.
   210  // Defaults to DefaultDataDir in the current working directory.
   211  func (m *Meta) DataDir() string {
   212  	if m.OverrideDataDir != "" {
   213  		return m.OverrideDataDir
   214  	}
   215  	return DefaultDataDir
   216  }
   217  
   218  const (
   219  	// InputModeEnvVar is the environment variable that, if set to "false" or
   220  	// "0", causes terraform commands to behave as if the `-input=false` flag was
   221  	// specified.
   222  	InputModeEnvVar = "TF_INPUT"
   223  )
   224  
   225  // InputMode returns the type of input we should ask for in the form of
   226  // terraform.InputMode which is passed directly to Context.Input.
   227  func (m *Meta) InputMode() terraform.InputMode {
   228  	if test || !m.input {
   229  		return 0
   230  	}
   231  
   232  	if envVar := os.Getenv(InputModeEnvVar); envVar != "" {
   233  		if v, err := strconv.ParseBool(envVar); err == nil {
   234  			if !v {
   235  				return 0
   236  			}
   237  		}
   238  	}
   239  
   240  	var mode terraform.InputMode
   241  	mode |= terraform.InputModeProvider
   242  	mode |= terraform.InputModeVar
   243  	mode |= terraform.InputModeVarUnset
   244  
   245  	return mode
   246  }
   247  
   248  // UIInput returns a UIInput object to be used for asking for input.
   249  func (m *Meta) UIInput() terraform.UIInput {
   250  	return &UIInput{
   251  		Colorize: m.Colorize(),
   252  	}
   253  }
   254  
   255  // StdinPiped returns true if the input is piped.
   256  func (m *Meta) StdinPiped() bool {
   257  	fi, err := wrappedstreams.Stdin().Stat()
   258  	if err != nil {
   259  		// If there is an error, let's just say its not piped
   260  		return false
   261  	}
   262  
   263  	return fi.Mode()&os.ModeNamedPipe != 0
   264  }
   265  
   266  const (
   267  	ProviderSkipVerifyEnvVar = "TF_SKIP_PROVIDER_VERIFY"
   268  )
   269  
   270  // contextOpts returns the options to use to initialize a Terraform
   271  // context with the settings from this Meta.
   272  func (m *Meta) contextOpts() *terraform.ContextOpts {
   273  	var opts terraform.ContextOpts
   274  	opts.Hooks = []terraform.Hook{m.uiHook(), &terraform.DebugHook{}}
   275  	opts.Hooks = append(opts.Hooks, m.ExtraHooks...)
   276  
   277  	vs := make(map[string]interface{})
   278  	for k, v := range opts.Variables {
   279  		vs[k] = v
   280  	}
   281  	for k, v := range m.autoVariables {
   282  		vs[k] = v
   283  	}
   284  	for k, v := range m.variables {
   285  		vs[k] = v
   286  	}
   287  	opts.Variables = vs
   288  
   289  	opts.Targets = m.targets
   290  	opts.UIInput = m.UIInput()
   291  	opts.Parallelism = m.parallelism
   292  	opts.Shadow = m.shadow
   293  
   294  	// If testingOverrides are set, we'll skip the plugin discovery process
   295  	// and just work with what we've been given, thus allowing the tests
   296  	// to provide mock providers and provisioners.
   297  	if m.testingOverrides != nil {
   298  		opts.ProviderResolver = m.testingOverrides.ProviderResolver
   299  		opts.Provisioners = m.testingOverrides.Provisioners
   300  	} else {
   301  		opts.ProviderResolver = m.providerResolver()
   302  		opts.Provisioners = m.provisionerFactories()
   303  	}
   304  
   305  	opts.ProviderSHA256s = m.providerPluginsLock().Read()
   306  	if v := os.Getenv(ProviderSkipVerifyEnvVar); v != "" {
   307  		opts.SkipProviderVerify = true
   308  	}
   309  
   310  	opts.Meta = &terraform.ContextMeta{
   311  		Env: m.Workspace(),
   312  	}
   313  
   314  	return &opts
   315  }
   316  
   317  // flags adds the meta flags to the given FlagSet.
   318  func (m *Meta) flagSet(n string) *flag.FlagSet {
   319  	f := flag.NewFlagSet(n, flag.ContinueOnError)
   320  	f.BoolVar(&m.input, "input", true, "input")
   321  	f.Var((*variables.Flag)(&m.variables), "var", "variables")
   322  	f.Var((*variables.FlagFile)(&m.variables), "var-file", "variable file")
   323  	f.Var((*FlagStringSlice)(&m.targets), "target", "resource to target")
   324  
   325  	if m.autoKey != "" {
   326  		f.Var((*variables.FlagFile)(&m.autoVariables), m.autoKey, "variable file")
   327  	}
   328  
   329  	// Advanced (don't need documentation, or unlikely to be set)
   330  	f.BoolVar(&m.shadow, "shadow", true, "shadow graph")
   331  
   332  	// Experimental features
   333  	experiment.Flag(f)
   334  
   335  	// Create an io.Writer that writes to our Ui properly for errors.
   336  	// This is kind of a hack, but it does the job. Basically: create
   337  	// a pipe, use a scanner to break it into lines, and output each line
   338  	// to the UI. Do this forever.
   339  
   340  	// If a previous pipe exists, we need to close that first.
   341  	// This should only happen in testing.
   342  	if m.errWriter != nil {
   343  		m.errWriter.Close()
   344  	}
   345  
   346  	if m.errScannerDone != nil {
   347  		<-m.errScannerDone
   348  	}
   349  
   350  	errR, errW := io.Pipe()
   351  	errScanner := bufio.NewScanner(errR)
   352  	m.errWriter = errW
   353  	m.errScannerDone = make(chan struct{})
   354  	go func() {
   355  		defer close(m.errScannerDone)
   356  		for errScanner.Scan() {
   357  			m.Ui.Error(errScanner.Text())
   358  		}
   359  	}()
   360  	f.SetOutput(errW)
   361  
   362  	// Set the default Usage to empty
   363  	f.Usage = func() {}
   364  
   365  	// command that bypass locking will supply their own flag on this var, but
   366  	// set the initial meta value to true as a failsafe.
   367  	m.stateLock = true
   368  
   369  	return f
   370  }
   371  
   372  // moduleStorage returns the module.Storage implementation used to store
   373  // modules for commands.
   374  func (m *Meta) moduleStorage(root string, mode module.GetMode) *module.Storage {
   375  	s := module.NewStorage(filepath.Join(root, "modules"), m.Services, m.Credentials)
   376  	s.Ui = m.Ui
   377  	s.Mode = mode
   378  	return s
   379  }
   380  
   381  // process will process the meta-parameters out of the arguments. This
   382  // will potentially modify the args in-place. It will return the resulting
   383  // slice.
   384  //
   385  // vars says whether or not we support variables.
   386  func (m *Meta) process(args []string, vars bool) ([]string, error) {
   387  	// We do this so that we retain the ability to technically call
   388  	// process multiple times, even if we have no plans to do so
   389  	if m.oldUi != nil {
   390  		m.Ui = m.oldUi
   391  	}
   392  
   393  	// Set colorization
   394  	m.color = m.Color
   395  	for i, v := range args {
   396  		if v == "-no-color" {
   397  			m.color = false
   398  			m.Color = false
   399  			args = append(args[:i], args[i+1:]...)
   400  			break
   401  		}
   402  	}
   403  
   404  	// Set the UI
   405  	m.oldUi = m.Ui
   406  	m.Ui = &cli.ConcurrentUi{
   407  		Ui: &ColorizeUi{
   408  			Colorize:   m.Colorize(),
   409  			ErrorColor: "[red]",
   410  			WarnColor:  "[yellow]",
   411  			Ui:         m.oldUi,
   412  		},
   413  	}
   414  
   415  	// If we support vars and the default var file exists, add it to
   416  	// the args...
   417  	m.autoKey = ""
   418  	if vars {
   419  		var preArgs []string
   420  
   421  		if _, err := os.Stat(DefaultVarsFilename); err == nil {
   422  			m.autoKey = "var-file-default"
   423  			preArgs = append(preArgs, "-"+m.autoKey, DefaultVarsFilename)
   424  		}
   425  
   426  		if _, err := os.Stat(DefaultVarsFilename + ".json"); err == nil {
   427  			m.autoKey = "var-file-default"
   428  			preArgs = append(preArgs, "-"+m.autoKey, DefaultVarsFilename+".json")
   429  		}
   430  
   431  		wd, err := os.Getwd()
   432  		if err != nil {
   433  			return nil, err
   434  		}
   435  
   436  		fis, err := ioutil.ReadDir(wd)
   437  		if err != nil {
   438  			return nil, err
   439  		}
   440  
   441  		// make sure we add the files in order
   442  		sort.Slice(fis, func(i, j int) bool {
   443  			return fis[i].Name() < fis[j].Name()
   444  		})
   445  
   446  		for _, fi := range fis {
   447  			name := fi.Name()
   448  			// Ignore directories, non-var-files, and ignored files
   449  			if fi.IsDir() || !isAutoVarFile(name) || config.IsIgnoredFile(name) {
   450  				continue
   451  			}
   452  
   453  			m.autoKey = "var-file-default"
   454  			preArgs = append(preArgs, "-"+m.autoKey, name)
   455  		}
   456  
   457  		args = append(preArgs, args...)
   458  	}
   459  
   460  	return args, nil
   461  }
   462  
   463  // uiHook returns the UiHook to use with the context.
   464  func (m *Meta) uiHook() *UiHook {
   465  	return &UiHook{
   466  		Colorize: m.Colorize(),
   467  		Ui:       m.Ui,
   468  	}
   469  }
   470  
   471  // confirm asks a yes/no confirmation.
   472  func (m *Meta) confirm(opts *terraform.InputOpts) (bool, error) {
   473  	if !m.Input() {
   474  		return false, errors.New("input is disabled")
   475  	}
   476  	for {
   477  		v, err := m.UIInput().Input(opts)
   478  		if err != nil {
   479  			return false, fmt.Errorf(
   480  				"Error asking for confirmation: %s", err)
   481  		}
   482  
   483  		switch strings.ToLower(v) {
   484  		case "no":
   485  			return false, nil
   486  		case "yes":
   487  			return true, nil
   488  		}
   489  	}
   490  }
   491  
   492  // showDiagnostics displays error and warning messages in the UI.
   493  //
   494  // "Diagnostics" here means the Diagnostics type from the tfdiag package,
   495  // though as a convenience this function accepts anything that could be
   496  // passed to the "Append" method on that type, converting it to Diagnostics
   497  // before displaying it.
   498  //
   499  // Internally this function uses Diagnostics.Append, and so it will panic
   500  // if given unsupported value types, just as Append does.
   501  func (m *Meta) showDiagnostics(vals ...interface{}) {
   502  	var diags tfdiags.Diagnostics
   503  	diags = diags.Append(vals...)
   504  
   505  	for _, diag := range diags {
   506  		// TODO: Actually measure the terminal width and pass it here.
   507  		// For now, we don't have easy access to the writer that
   508  		// ui.Error (etc) are writing to and thus can't interrogate
   509  		// to see if it's a terminal and what size it is.
   510  		msg := format.Diagnostic(diag, m.Colorize(), 78)
   511  		switch diag.Severity() {
   512  		case tfdiags.Error:
   513  			m.Ui.Error(msg)
   514  		case tfdiags.Warning:
   515  			m.Ui.Warn(msg)
   516  		default:
   517  			m.Ui.Output(msg)
   518  		}
   519  	}
   520  }
   521  
   522  const (
   523  	// ModuleDepthDefault is the default value for
   524  	// module depth, which can be overridden by flag
   525  	// or env var
   526  	ModuleDepthDefault = -1
   527  
   528  	// ModuleDepthEnvVar is the name of the environment variable that can be used to set module depth.
   529  	ModuleDepthEnvVar = "TF_MODULE_DEPTH"
   530  )
   531  
   532  func (m *Meta) addModuleDepthFlag(flags *flag.FlagSet, moduleDepth *int) {
   533  	flags.IntVar(moduleDepth, "module-depth", ModuleDepthDefault, "module-depth")
   534  	if envVar := os.Getenv(ModuleDepthEnvVar); envVar != "" {
   535  		if md, err := strconv.Atoi(envVar); err == nil {
   536  			*moduleDepth = md
   537  		}
   538  	}
   539  }
   540  
   541  // outputShadowError outputs the error from ctx.ShadowError. If the
   542  // error is nil then nothing happens. If output is false then it isn't
   543  // outputted to the user (you can define logic to guard against outputting).
   544  func (m *Meta) outputShadowError(err error, output bool) bool {
   545  	// Do nothing if no error
   546  	if err == nil {
   547  		return false
   548  	}
   549  
   550  	// If not outputting, do nothing
   551  	if !output {
   552  		return false
   553  	}
   554  
   555  	// Write the shadow error output to a file
   556  	path := fmt.Sprintf("terraform-error-%d.log", time.Now().UTC().Unix())
   557  	if err := ioutil.WriteFile(path, []byte(err.Error()), 0644); err != nil {
   558  		// If there is an error writing it, just let it go
   559  		log.Printf("[ERROR] Error writing shadow error: %s", err)
   560  		return false
   561  	}
   562  
   563  	// Output!
   564  	m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
   565  		"[reset][bold][yellow]\nExperimental feature failure! Please report a bug.\n\n"+
   566  			"This is not an error. Your Terraform operation completed successfully.\n"+
   567  			"Your real infrastructure is unaffected by this message.\n\n"+
   568  			"[reset][yellow]While running, Terraform sometimes tests experimental features in the\n"+
   569  			"background. These features cannot affect real state and never touch\n"+
   570  			"real infrastructure. If the features work properly, you see nothing.\n"+
   571  			"If the features fail, this message appears.\n\n"+
   572  			"You can report an issue at: https://github.com/hashicorp/terraform/issues\n\n"+
   573  			"The failure was written to %q. Please\n"+
   574  			"double check this file contains no sensitive information and report\n"+
   575  			"it with your issue.\n\n"+
   576  			"This is not an error. Your terraform operation completed successfully\n"+
   577  			"and your real infrastructure is unaffected by this message.",
   578  		path,
   579  	)))
   580  
   581  	return true
   582  }
   583  
   584  // WorkspaceNameEnvVar is the name of the environment variable that can be used
   585  // to set the name of the Terraform workspace, overriding the workspace chosen
   586  // by `terraform workspace select`.
   587  //
   588  // Note that this environment variable is ignored by `terraform workspace new`
   589  // and `terraform workspace delete`.
   590  const WorkspaceNameEnvVar = "TF_WORKSPACE"
   591  
   592  // Workspace returns the name of the currently configured workspace, corresponding
   593  // to the desired named state.
   594  func (m *Meta) Workspace() string {
   595  	current, _ := m.WorkspaceOverridden()
   596  	return current
   597  }
   598  
   599  // WorkspaceOverridden returns the name of the currently configured workspace,
   600  // corresponding to the desired named state, as well as a bool saying whether
   601  // this was set via the TF_WORKSPACE environment variable.
   602  func (m *Meta) WorkspaceOverridden() (string, bool) {
   603  	if envVar := os.Getenv(WorkspaceNameEnvVar); envVar != "" {
   604  		return envVar, true
   605  	}
   606  
   607  	envData, err := ioutil.ReadFile(filepath.Join(m.DataDir(), local.DefaultWorkspaceFile))
   608  	current := string(bytes.TrimSpace(envData))
   609  	if current == "" {
   610  		current = backend.DefaultStateName
   611  	}
   612  
   613  	if err != nil && !os.IsNotExist(err) {
   614  		// always return the default if we can't get a workspace name
   615  		log.Printf("[ERROR] failed to read current workspace: %s", err)
   616  	}
   617  
   618  	return current, false
   619  }
   620  
   621  // SetWorkspace saves the given name as the current workspace in the local
   622  // filesystem.
   623  func (m *Meta) SetWorkspace(name string) error {
   624  	err := os.MkdirAll(m.DataDir(), 0755)
   625  	if err != nil {
   626  		return err
   627  	}
   628  
   629  	err = ioutil.WriteFile(filepath.Join(m.DataDir(), local.DefaultWorkspaceFile), []byte(name), 0644)
   630  	if err != nil {
   631  		return err
   632  	}
   633  	return nil
   634  }
   635  
   636  // isAutoVarFile determines if the file ends with .auto.tfvars or .auto.tfvars.json
   637  func isAutoVarFile(path string) bool {
   638  	return strings.HasSuffix(path, ".auto.tfvars") ||
   639  		strings.HasSuffix(path, ".auto.tfvars.json")
   640  }