github.com/ns1/terraform@v0.7.10-0.20161109153551-8949419bef40/command/meta.go (about)

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