github.com/tarrant/terraform@v0.3.8-0.20150402012457-f68c9eee638e/command/meta.go (about)

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