github.com/recobe182/terraform@v0.8.5-0.20170117231232-49ab22a935b7/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  	"time"
    14  
    15  	"github.com/hashicorp/errwrap"
    16  	"github.com/hashicorp/go-getter"
    17  	"github.com/hashicorp/terraform/config"
    18  	"github.com/hashicorp/terraform/config/module"
    19  	"github.com/hashicorp/terraform/helper/experiment"
    20  	"github.com/hashicorp/terraform/helper/variables"
    21  	"github.com/hashicorp/terraform/helper/wrappedstreams"
    22  	"github.com/hashicorp/terraform/state"
    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  	Color       bool
    31  	ContextOpts *terraform.ContextOpts
    32  	Ui          cli.Ui
    33  
    34  	// State read when calling `Context`. This is available after calling
    35  	// `Context`.
    36  	state       state.State
    37  	stateResult *StateResult
    38  
    39  	// This can be set by the command itself to provide extra hooks.
    40  	extraHooks []terraform.Hook
    41  
    42  	// This can be set by tests to change some directories
    43  	dataDir string
    44  
    45  	// Variables for the context (private)
    46  	autoKey       string
    47  	autoVariables map[string]interface{}
    48  	input         bool
    49  	variables     map[string]interface{}
    50  
    51  	// Targets for this context (private)
    52  	targets []string
    53  
    54  	color bool
    55  	oldUi cli.Ui
    56  
    57  	// The fields below are expected to be set by the command via
    58  	// command line flags. See the Apply command for an example.
    59  	//
    60  	// statePath is the path to the state file. If this is empty, then
    61  	// no state will be loaded. It is also okay for this to be a path to
    62  	// a file that doesn't exist; it is assumed that this means that there
    63  	// is simply no state.
    64  	//
    65  	// stateOutPath is used to override the output path for the state.
    66  	// If not provided, the StatePath is used causing the old state to
    67  	// be overriden.
    68  	//
    69  	// backupPath is used to backup the state file before writing a modified
    70  	// version. It defaults to stateOutPath + DefaultBackupExtension
    71  	//
    72  	// parallelism is used to control the number of concurrent operations
    73  	// allowed when walking the graph
    74  	//
    75  	// shadow is used to enable/disable the shadow graph
    76  	//
    77  	// provider is to specify specific resource providers
    78  	statePath    string
    79  	stateOutPath string
    80  	backupPath   string
    81  	parallelism  int
    82  	shadow       bool
    83  	provider     string
    84  }
    85  
    86  // initStatePaths is used to initialize the default values for
    87  // statePath, stateOutPath, and backupPath
    88  func (m *Meta) initStatePaths() {
    89  	if m.statePath == "" {
    90  		m.statePath = DefaultStateFilename
    91  	}
    92  	if m.stateOutPath == "" {
    93  		m.stateOutPath = m.statePath
    94  	}
    95  	if m.backupPath == "" {
    96  		m.backupPath = m.stateOutPath + DefaultBackupExtension
    97  	}
    98  }
    99  
   100  // StateOutPath returns the true output path for the state file
   101  func (m *Meta) StateOutPath() string {
   102  	return m.stateOutPath
   103  }
   104  
   105  // Colorize returns the colorization structure for a command.
   106  func (m *Meta) Colorize() *colorstring.Colorize {
   107  	return &colorstring.Colorize{
   108  		Colors:  colorstring.DefaultColors,
   109  		Disable: !m.color,
   110  		Reset:   true,
   111  	}
   112  }
   113  
   114  // Context returns a Terraform Context taking into account the context
   115  // options used to initialize this meta configuration.
   116  func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
   117  	opts := m.contextOpts()
   118  
   119  	// First try to just read the plan directly from the path given.
   120  	f, err := os.Open(copts.Path)
   121  	if err == nil {
   122  		plan, err := terraform.ReadPlan(f)
   123  		f.Close()
   124  		if err == nil {
   125  			// Setup our state, force it to use our plan's state
   126  			stateOpts := m.StateOpts()
   127  			if plan != nil {
   128  				stateOpts.ForceState = plan.State
   129  			}
   130  
   131  			// Get the state
   132  			result, err := State(stateOpts)
   133  			if err != nil {
   134  				return nil, false, fmt.Errorf("Error loading plan: %s", err)
   135  			}
   136  
   137  			// Set our state
   138  			m.state = result.State
   139  
   140  			// this is used for printing the saved location later
   141  			if m.stateOutPath == "" {
   142  				m.stateOutPath = result.StatePath
   143  			}
   144  
   145  			if len(m.variables) > 0 {
   146  				return nil, false, fmt.Errorf(
   147  					"You can't set variables with the '-var' or '-var-file' flag\n" +
   148  						"when you're applying a plan file. The variables used when\n" +
   149  						"the plan was created will be used. If you wish to use different\n" +
   150  						"variable values, create a new plan file.")
   151  			}
   152  
   153  			ctx, err := plan.Context(opts)
   154  			return ctx, true, err
   155  		}
   156  	}
   157  
   158  	// Load the statePath if not given
   159  	if copts.StatePath != "" {
   160  		m.statePath = copts.StatePath
   161  	}
   162  
   163  	// Tell the context if we're in a destroy plan / apply
   164  	opts.Destroy = copts.Destroy
   165  
   166  	// Store the loaded state
   167  	state, err := m.State()
   168  	if err != nil {
   169  		return nil, false, err
   170  	}
   171  
   172  	// Load the root module
   173  	var mod *module.Tree
   174  	if copts.Path != "" {
   175  		mod, err = module.NewTreeModule("", copts.Path)
   176  
   177  		// Check for the error where we have no config files but
   178  		// allow that. If that happens, clear the error.
   179  		if errwrap.ContainsType(err, new(config.ErrNoConfigsFound)) &&
   180  			copts.PathEmptyOk {
   181  			log.Printf(
   182  				"[WARN] Empty configuration dir, ignoring: %s", copts.Path)
   183  			err = nil
   184  			mod = module.NewEmptyTree()
   185  		}
   186  
   187  		if err != nil {
   188  			return nil, false, fmt.Errorf("Error loading config: %s", err)
   189  		}
   190  	} else {
   191  		mod = module.NewEmptyTree()
   192  	}
   193  
   194  	err = mod.Load(m.moduleStorage(m.DataDir()), copts.GetMode)
   195  	if err != nil {
   196  		return nil, false, fmt.Errorf("Error downloading modules: %s", err)
   197  	}
   198  
   199  	// Validate the module right away
   200  	if err := mod.Validate(); err != nil {
   201  		return nil, false, err
   202  	}
   203  
   204  	opts.Module = mod
   205  	opts.Parallelism = copts.Parallelism
   206  	opts.State = state.State()
   207  	ctx, err := terraform.NewContext(opts)
   208  	return ctx, false, err
   209  }
   210  
   211  // DataDir returns the directory where local data will be stored.
   212  func (m *Meta) DataDir() string {
   213  	dataDir := DefaultDataDir
   214  	if m.dataDir != "" {
   215  		dataDir = m.dataDir
   216  	}
   217  
   218  	return dataDir
   219  }
   220  
   221  const (
   222  	// InputModeEnvVar is the environment variable that, if set to "false" or
   223  	// "0", causes terraform commands to behave as if the `-input=false` flag was
   224  	// specified.
   225  	InputModeEnvVar = "TF_INPUT"
   226  )
   227  
   228  // InputMode returns the type of input we should ask for in the form of
   229  // terraform.InputMode which is passed directly to Context.Input.
   230  func (m *Meta) InputMode() terraform.InputMode {
   231  	if test || !m.input {
   232  		return 0
   233  	}
   234  
   235  	if envVar := os.Getenv(InputModeEnvVar); envVar != "" {
   236  		if v, err := strconv.ParseBool(envVar); err == nil {
   237  			if !v {
   238  				return 0
   239  			}
   240  		}
   241  	}
   242  
   243  	var mode terraform.InputMode
   244  	mode |= terraform.InputModeProvider
   245  	mode |= terraform.InputModeVar
   246  	mode |= terraform.InputModeVarUnset
   247  
   248  	return mode
   249  }
   250  
   251  // State returns the state for this meta.
   252  func (m *Meta) State() (state.State, error) {
   253  	if m.state != nil {
   254  		return m.state, nil
   255  	}
   256  
   257  	result, err := State(m.StateOpts())
   258  	if err != nil {
   259  		return nil, err
   260  	}
   261  
   262  	m.state = result.State
   263  	m.stateOutPath = result.StatePath
   264  	m.stateResult = result
   265  	return m.state, nil
   266  }
   267  
   268  // StateRaw is used to setup the state manually.
   269  func (m *Meta) StateRaw(opts *StateOpts) (*StateResult, error) {
   270  	result, err := State(opts)
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  
   275  	m.state = result.State
   276  	m.stateOutPath = result.StatePath
   277  	m.stateResult = result
   278  	return result, nil
   279  }
   280  
   281  // StateOpts returns the default state options
   282  func (m *Meta) StateOpts() *StateOpts {
   283  	localPath := m.statePath
   284  	if localPath == "" {
   285  		localPath = DefaultStateFilename
   286  	}
   287  	remotePath := filepath.Join(m.DataDir(), DefaultStateFilename)
   288  
   289  	return &StateOpts{
   290  		LocalPath:     localPath,
   291  		LocalPathOut:  m.stateOutPath,
   292  		RemotePath:    remotePath,
   293  		RemoteRefresh: true,
   294  		BackupPath:    m.backupPath,
   295  	}
   296  }
   297  
   298  // UIInput returns a UIInput object to be used for asking for input.
   299  func (m *Meta) UIInput() terraform.UIInput {
   300  	return &UIInput{
   301  		Colorize: m.Colorize(),
   302  	}
   303  }
   304  
   305  // PersistState is used to write out the state, handling backup of
   306  // the existing state file and respecting path configurations.
   307  func (m *Meta) PersistState(s *terraform.State) error {
   308  	if err := m.state.WriteState(s); err != nil {
   309  		return err
   310  	}
   311  
   312  	return m.state.PersistState()
   313  }
   314  
   315  // Input returns true if we should ask for input for context.
   316  func (m *Meta) Input() bool {
   317  	return !test && m.input && len(m.variables) == 0
   318  }
   319  
   320  // StdinPiped returns true if the input is piped.
   321  func (m *Meta) StdinPiped() bool {
   322  	fi, err := wrappedstreams.Stdin().Stat()
   323  	if err != nil {
   324  		// If there is an error, let's just say its not piped
   325  		return false
   326  	}
   327  
   328  	return fi.Mode()&os.ModeNamedPipe != 0
   329  }
   330  
   331  // contextOpts returns the options to use to initialize a Terraform
   332  // context with the settings from this Meta.
   333  func (m *Meta) contextOpts() *terraform.ContextOpts {
   334  	var opts terraform.ContextOpts = *m.ContextOpts
   335  
   336  	opts.Hooks = []terraform.Hook{m.uiHook(), &terraform.DebugHook{}}
   337  	opts.Hooks = append(opts.Hooks, m.ContextOpts.Hooks...)
   338  	opts.Hooks = append(opts.Hooks, m.extraHooks...)
   339  
   340  	vs := make(map[string]interface{})
   341  	for k, v := range opts.Variables {
   342  		vs[k] = v
   343  	}
   344  	for k, v := range m.autoVariables {
   345  		vs[k] = v
   346  	}
   347  	for k, v := range m.variables {
   348  		vs[k] = v
   349  	}
   350  	opts.Variables = vs
   351  	opts.Targets = m.targets
   352  	opts.UIInput = m.UIInput()
   353  	opts.Shadow = m.shadow
   354  
   355  	return &opts
   356  }
   357  
   358  // flags adds the meta flags to the given FlagSet.
   359  func (m *Meta) flagSet(n string) *flag.FlagSet {
   360  	f := flag.NewFlagSet(n, flag.ContinueOnError)
   361  	f.BoolVar(&m.input, "input", true, "input")
   362  	f.Var((*variables.Flag)(&m.variables), "var", "variables")
   363  	f.Var((*variables.FlagFile)(&m.variables), "var-file", "variable file")
   364  	f.Var((*FlagStringSlice)(&m.targets), "target", "resource to target")
   365  
   366  	if m.autoKey != "" {
   367  		f.Var((*variables.FlagFile)(&m.autoVariables), m.autoKey, "variable file")
   368  	}
   369  
   370  	// Advanced (don't need documentation, or unlikely to be set)
   371  	f.BoolVar(&m.shadow, "shadow", true, "shadow graph")
   372  
   373  	// Experimental features
   374  	experiment.Flag(f)
   375  
   376  	// Create an io.Writer that writes to our Ui properly for errors.
   377  	// This is kind of a hack, but it does the job. Basically: create
   378  	// a pipe, use a scanner to break it into lines, and output each line
   379  	// to the UI. Do this forever.
   380  	errR, errW := io.Pipe()
   381  	errScanner := bufio.NewScanner(errR)
   382  	go func() {
   383  		for errScanner.Scan() {
   384  			m.Ui.Error(errScanner.Text())
   385  		}
   386  	}()
   387  	f.SetOutput(errW)
   388  
   389  	// Set the default Usage to empty
   390  	f.Usage = func() {}
   391  
   392  	return f
   393  }
   394  
   395  // moduleStorage returns the module.Storage implementation used to store
   396  // modules for commands.
   397  func (m *Meta) moduleStorage(root string) getter.Storage {
   398  	return &uiModuleStorage{
   399  		Storage: &getter.FolderStorage{
   400  			StorageDir: filepath.Join(root, "modules"),
   401  		},
   402  		Ui: m.Ui,
   403  	}
   404  }
   405  
   406  // process will process the meta-parameters out of the arguments. This
   407  // will potentially modify the args in-place. It will return the resulting
   408  // slice.
   409  //
   410  // vars says whether or not we support variables.
   411  func (m *Meta) process(args []string, vars bool) []string {
   412  	// We do this so that we retain the ability to technically call
   413  	// process multiple times, even if we have no plans to do so
   414  	if m.oldUi != nil {
   415  		m.Ui = m.oldUi
   416  	}
   417  
   418  	// Set colorization
   419  	m.color = m.Color
   420  	for i, v := range args {
   421  		if v == "-no-color" {
   422  			m.color = false
   423  			m.Color = false
   424  			args = append(args[:i], args[i+1:]...)
   425  			break
   426  		}
   427  	}
   428  
   429  	// Set the UI
   430  	m.oldUi = m.Ui
   431  	m.Ui = &cli.ConcurrentUi{
   432  		Ui: &ColorizeUi{
   433  			Colorize:   m.Colorize(),
   434  			ErrorColor: "[red]",
   435  			WarnColor:  "[yellow]",
   436  			Ui:         m.oldUi,
   437  		},
   438  	}
   439  
   440  	// If we support vars and the default var file exists, add it to
   441  	// the args...
   442  	m.autoKey = ""
   443  	if vars {
   444  		if _, err := os.Stat(DefaultVarsFilename); err == nil {
   445  			m.autoKey = "var-file-default"
   446  			args = append(args, "", "")
   447  			copy(args[2:], args[0:])
   448  			args[0] = "-" + m.autoKey
   449  			args[1] = DefaultVarsFilename
   450  		}
   451  
   452  		if _, err := os.Stat(DefaultVarsFilename + ".json"); err == nil {
   453  			m.autoKey = "var-file-default"
   454  			args = append(args, "", "")
   455  			copy(args[2:], args[0:])
   456  			args[0] = "-" + m.autoKey
   457  			args[1] = DefaultVarsFilename + ".json"
   458  		}
   459  	}
   460  
   461  	return args
   462  }
   463  
   464  // uiHook returns the UiHook to use with the context.
   465  func (m *Meta) uiHook() *UiHook {
   466  	return &UiHook{
   467  		Colorize: m.Colorize(),
   468  		Ui:       m.Ui,
   469  	}
   470  }
   471  
   472  const (
   473  	// ModuleDepthDefault is the default value for
   474  	// module depth, which can be overridden by flag
   475  	// or env var
   476  	ModuleDepthDefault = -1
   477  
   478  	// ModuleDepthEnvVar is the name of the environment variable that can be used to set module depth.
   479  	ModuleDepthEnvVar = "TF_MODULE_DEPTH"
   480  )
   481  
   482  func (m *Meta) addModuleDepthFlag(flags *flag.FlagSet, moduleDepth *int) {
   483  	flags.IntVar(moduleDepth, "module-depth", ModuleDepthDefault, "module-depth")
   484  	if envVar := os.Getenv(ModuleDepthEnvVar); envVar != "" {
   485  		if md, err := strconv.Atoi(envVar); err == nil {
   486  			*moduleDepth = md
   487  		}
   488  	}
   489  }
   490  
   491  // outputShadowError outputs the error from ctx.ShadowError. If the
   492  // error is nil then nothing happens. If output is false then it isn't
   493  // outputted to the user (you can define logic to guard against outputting).
   494  func (m *Meta) outputShadowError(err error, output bool) bool {
   495  	// Do nothing if no error
   496  	if err == nil {
   497  		return false
   498  	}
   499  
   500  	// If not outputting, do nothing
   501  	if !output {
   502  		return false
   503  	}
   504  
   505  	// Write the shadow error output to a file
   506  	path := fmt.Sprintf("terraform-error-%d.log", time.Now().UTC().Unix())
   507  	if err := ioutil.WriteFile(path, []byte(err.Error()), 0644); err != nil {
   508  		// If there is an error writing it, just let it go
   509  		log.Printf("[ERROR] Error writing shadow error: %s", err)
   510  		return false
   511  	}
   512  
   513  	// Output!
   514  	m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
   515  		"[reset][bold][yellow]\nExperimental feature failure! Please report a bug.\n\n"+
   516  			"This is not an error. Your Terraform operation completed successfully.\n"+
   517  			"Your real infrastructure is unaffected by this message.\n\n"+
   518  			"[reset][yellow]While running, Terraform sometimes tests experimental features in the\n"+
   519  			"background. These features cannot affect real state and never touch\n"+
   520  			"real infrastructure. If the features work properly, you see nothing.\n"+
   521  			"If the features fail, this message appears.\n\n"+
   522  			"You can report an issue at: https://github.com/hashicorp/terraform/issues\n\n"+
   523  			"The failure was written to %q. Please\n"+
   524  			"double check this file contains no sensitive information and report\n"+
   525  			"it with your issue.\n\n"+
   526  			"This is not an error. Your terraform operation completed successfully\n"+
   527  			"and your real infrastructure is unaffected by this message.",
   528  		path,
   529  	)))
   530  
   531  	return true
   532  }
   533  
   534  // contextOpts are the options used to load a context from a command.
   535  type contextOpts struct {
   536  	// Path to the directory where the root module is.
   537  	//
   538  	// PathEmptyOk, when set, will allow paths that have no Terraform
   539  	// configurations. The result in that case will be an empty module.
   540  	Path        string
   541  	PathEmptyOk bool
   542  
   543  	// StatePath is the path to the state file. If this is empty, then
   544  	// no state will be loaded. It is also okay for this to be a path to
   545  	// a file that doesn't exist; it is assumed that this means that there
   546  	// is simply no state.
   547  	StatePath string
   548  
   549  	// GetMode is the module.GetMode to use when loading the module tree.
   550  	GetMode module.GetMode
   551  
   552  	// Set to true when running a destroy plan/apply.
   553  	Destroy bool
   554  
   555  	// Number of concurrent operations allowed
   556  	Parallelism int
   557  }