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