github.com/mkuzmin/terraform@v0.3.7-0.20161118171027-ec4c00ff92a9/command/meta.go (about)

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