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