github.com/chalford/terraform@v0.3.7-0.20150113080010-a78c69a8c81f/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  
    12  	"github.com/hashicorp/terraform/config/module"
    13  	"github.com/hashicorp/terraform/remote"
    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 *terraform.State
    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  	color bool
    42  	oldUi cli.Ui
    43  
    44  	// useRemoteState is enabled if we are using remote state storage
    45  	// This is set when the context is loaded if we read from a remote
    46  	// enabled state file.
    47  	useRemoteState bool
    48  
    49  	// statePath is the path to the state file. If this is empty, then
    50  	// no state will be loaded. It is also okay for this to be a path to
    51  	// a file that doesn't exist; it is assumed that this means that there
    52  	// is simply no state.
    53  	statePath string
    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  	stateOutPath string
    59  
    60  	// backupPath is used to backup the state file before writing a modified
    61  	// version. It defaults to stateOutPath + DefaultBackupExtention
    62  	backupPath string
    63  }
    64  
    65  // initStatePaths is used to initialize the default values for
    66  // statePath, stateOutPath, and backupPath
    67  func (m *Meta) initStatePaths() {
    68  	if m.statePath == "" {
    69  		m.statePath = DefaultStateFilename
    70  	}
    71  	if m.stateOutPath == "" {
    72  		m.stateOutPath = m.statePath
    73  	}
    74  	if m.backupPath == "" {
    75  		m.backupPath = m.stateOutPath + DefaultBackupExtention
    76  	}
    77  }
    78  
    79  // StateOutPath returns the true output path for the state file
    80  func (m *Meta) StateOutPath() string {
    81  	m.initStatePaths()
    82  	if m.useRemoteState {
    83  		path, _ := remote.HiddenStatePath()
    84  		return path
    85  	}
    86  	return m.stateOutPath
    87  }
    88  
    89  // Colorize returns the colorization structure for a command.
    90  func (m *Meta) Colorize() *colorstring.Colorize {
    91  	return &colorstring.Colorize{
    92  		Colors:  colorstring.DefaultColors,
    93  		Disable: !m.color,
    94  		Reset:   true,
    95  	}
    96  }
    97  
    98  // Context returns a Terraform Context taking into account the context
    99  // options used to initialize this meta configuration.
   100  func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
   101  	opts := m.contextOpts()
   102  
   103  	// First try to just read the plan directly from the path given.
   104  	f, err := os.Open(copts.Path)
   105  	if err == nil {
   106  		plan, err := terraform.ReadPlan(f)
   107  		f.Close()
   108  		if err == nil {
   109  			// Check if remote state is enabled, but do not refresh.
   110  			// Since a plan is supposed to lock-in the changes, we do not
   111  			// attempt a state refresh.
   112  			if plan != nil && plan.State != nil && plan.State.Remote != nil && plan.State.Remote.Type != "" {
   113  				log.Printf("[INFO] Enabling remote state from plan")
   114  				m.useRemoteState = true
   115  			}
   116  
   117  			if len(m.variables) > 0 {
   118  				return nil, false, fmt.Errorf(
   119  					"You can't set variables with the '-var' or '-var-file' flag\n" +
   120  						"when you're applying a plan file. The variables used when\n" +
   121  						"the plan was created will be used. If you wish to use different\n" +
   122  						"variable values, create a new plan file.")
   123  			}
   124  
   125  			return plan.Context(opts), true, nil
   126  		}
   127  	}
   128  
   129  	// Load the statePath if not given
   130  	if copts.StatePath != "" {
   131  		m.statePath = copts.StatePath
   132  	}
   133  
   134  	// Store the loaded state
   135  	state, err := m.loadState()
   136  	if err != nil {
   137  		return nil, false, err
   138  	}
   139  	m.state = state
   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  	dataDir := DefaultDataDirectory
   148  	if m.dataDir != "" {
   149  		dataDir = m.dataDir
   150  	}
   151  	err = mod.Load(m.moduleStorage(dataDir), copts.GetMode)
   152  	if err != nil {
   153  		return nil, false, fmt.Errorf("Error downloading modules: %s", err)
   154  	}
   155  
   156  	opts.Module = mod
   157  	opts.State = state
   158  	ctx := terraform.NewContext(opts)
   159  	return ctx, false, nil
   160  }
   161  
   162  // InputMode returns the type of input we should ask for in the form of
   163  // terraform.InputMode which is passed directly to Context.Input.
   164  func (m *Meta) InputMode() terraform.InputMode {
   165  	if test || !m.input {
   166  		return 0
   167  	}
   168  
   169  	var mode terraform.InputMode
   170  	mode |= terraform.InputModeProvider
   171  	if len(m.variables) == 0 && m.autoKey == "" {
   172  		mode |= terraform.InputModeVar
   173  	}
   174  
   175  	return mode
   176  }
   177  
   178  // UIInput returns a UIInput object to be used for asking for input.
   179  func (m *Meta) UIInput() terraform.UIInput {
   180  	return &UIInput{
   181  		Colorize: m.Colorize(),
   182  	}
   183  }
   184  
   185  // laodState is used to load the Terraform state. We give precedence
   186  // to a remote state if enabled, and then check the normal state path.
   187  func (m *Meta) loadState() (*terraform.State, error) {
   188  	// Check if we remote state is enabled
   189  	localCache, _, err := remote.ReadLocalState()
   190  	if err != nil {
   191  		return nil, fmt.Errorf("Error loading state: %s", err)
   192  	}
   193  
   194  	// Set the state if enabled
   195  	var state *terraform.State
   196  	if localCache != nil {
   197  		// Refresh the state
   198  		log.Printf("[INFO] Refreshing local state...")
   199  		changes, err := remote.RefreshState(localCache.Remote)
   200  		if err != nil {
   201  			return nil, fmt.Errorf("Failed to refresh state: %v", err)
   202  		}
   203  		switch changes {
   204  		case remote.StateChangeNoop:
   205  		case remote.StateChangeInit:
   206  		case remote.StateChangeLocalNewer:
   207  		case remote.StateChangeUpdateLocal:
   208  			// Reload the state since we've udpated
   209  			localCache, _, err = remote.ReadLocalState()
   210  			if err != nil {
   211  				return nil, fmt.Errorf("Error loading state: %s", err)
   212  			}
   213  		default:
   214  			return nil, fmt.Errorf("%s", changes)
   215  		}
   216  
   217  		state = localCache
   218  		m.useRemoteState = true
   219  	}
   220  
   221  	// Load up the state
   222  	if m.statePath != "" {
   223  		f, err := os.Open(m.statePath)
   224  		if err != nil && os.IsNotExist(err) {
   225  			// If the state file doesn't exist, it is okay, since it
   226  			// is probably a new infrastructure.
   227  			err = nil
   228  		} else if m.useRemoteState && err == nil {
   229  			err = fmt.Errorf("Remote state enabled, but state file '%s' also present.", m.statePath)
   230  			f.Close()
   231  		} else if err == nil {
   232  			state, err = terraform.ReadState(f)
   233  			f.Close()
   234  		}
   235  		if err != nil {
   236  			return nil, fmt.Errorf("Error loading state: %s", err)
   237  		}
   238  	}
   239  	return state, nil
   240  }
   241  
   242  // PersistState is used to write out the state, handling backup of
   243  // the existing state file and respecting path configurations.
   244  func (m *Meta) PersistState(s *terraform.State) error {
   245  	if m.useRemoteState {
   246  		return m.persistRemoteState(s)
   247  	}
   248  	return m.persistLocalState(s)
   249  }
   250  
   251  // persistRemoteState is used to handle persisting a state file
   252  // when remote state management is enabled
   253  func (m *Meta) persistRemoteState(s *terraform.State) error {
   254  	log.Printf("[INFO] Persisting state to local cache")
   255  	if err := remote.PersistState(s); err != nil {
   256  		return err
   257  	}
   258  	log.Printf("[INFO] Uploading state to remote store")
   259  	change, err := remote.PushState(s.Remote, false)
   260  	if err != nil {
   261  		return err
   262  	}
   263  	if !change.SuccessfulPush() {
   264  		return fmt.Errorf("Failed to upload state: %s", change)
   265  	}
   266  	return nil
   267  }
   268  
   269  // persistLocalState is used to handle persisting a state file
   270  // when remote state management is disabled.
   271  func (m *Meta) persistLocalState(s *terraform.State) error {
   272  	m.initStatePaths()
   273  
   274  	// Create a backup of the state before updating
   275  	if m.backupPath != "-" {
   276  		log.Printf("[INFO] Writing backup state to: %s", m.backupPath)
   277  		if err := remote.CopyFile(m.statePath, m.backupPath); err != nil {
   278  			return fmt.Errorf("Failed to backup state: %v", err)
   279  		}
   280  	}
   281  
   282  	// Open the new state file
   283  	fh, err := os.Create(m.stateOutPath)
   284  	if err != nil {
   285  		return fmt.Errorf("Failed to open state file: %v", err)
   286  	}
   287  	defer fh.Close()
   288  
   289  	// Write out the state
   290  	if err := terraform.WriteState(s, fh); err != nil {
   291  		return fmt.Errorf("Failed to encode the state: %v", err)
   292  	}
   293  	return nil
   294  }
   295  
   296  // Input returns true if we should ask for input for context.
   297  func (m *Meta) Input() bool {
   298  	return !test && m.input && len(m.variables) == 0
   299  }
   300  
   301  // contextOpts returns the options to use to initialize a Terraform
   302  // context with the settings from this Meta.
   303  func (m *Meta) contextOpts() *terraform.ContextOpts {
   304  	var opts terraform.ContextOpts = *m.ContextOpts
   305  	opts.Hooks = make(
   306  		[]terraform.Hook,
   307  		len(m.ContextOpts.Hooks)+len(m.extraHooks)+1)
   308  	opts.Hooks[0] = m.uiHook()
   309  	copy(opts.Hooks[1:], m.ContextOpts.Hooks)
   310  	copy(opts.Hooks[len(m.ContextOpts.Hooks)+1:], m.extraHooks)
   311  
   312  	vs := make(map[string]string)
   313  	for k, v := range opts.Variables {
   314  		vs[k] = v
   315  	}
   316  	for k, v := range m.autoVariables {
   317  		vs[k] = v
   318  	}
   319  	for k, v := range m.variables {
   320  		vs[k] = v
   321  	}
   322  	opts.Variables = vs
   323  	opts.UIInput = m.UIInput()
   324  
   325  	return &opts
   326  }
   327  
   328  // flags adds the meta flags to the given FlagSet.
   329  func (m *Meta) flagSet(n string) *flag.FlagSet {
   330  	f := flag.NewFlagSet(n, flag.ContinueOnError)
   331  	f.BoolVar(&m.input, "input", true, "input")
   332  	f.Var((*FlagVar)(&m.variables), "var", "variables")
   333  	f.Var((*FlagVarFile)(&m.variables), "var-file", "variable file")
   334  
   335  	if m.autoKey != "" {
   336  		f.Var((*FlagVarFile)(&m.autoVariables), m.autoKey, "variable file")
   337  	}
   338  
   339  	// Create an io.Writer that writes to our Ui properly for errors.
   340  	// This is kind of a hack, but it does the job. Basically: create
   341  	// a pipe, use a scanner to break it into lines, and output each line
   342  	// to the UI. Do this forever.
   343  	errR, errW := io.Pipe()
   344  	errScanner := bufio.NewScanner(errR)
   345  	go func() {
   346  		for errScanner.Scan() {
   347  			m.Ui.Error(errScanner.Text())
   348  		}
   349  	}()
   350  	f.SetOutput(errW)
   351  
   352  	return f
   353  }
   354  
   355  // moduleStorage returns the module.Storage implementation used to store
   356  // modules for commands.
   357  func (m *Meta) moduleStorage(root string) module.Storage {
   358  	return &uiModuleStorage{
   359  		Storage: &module.FolderStorage{
   360  			StorageDir: filepath.Join(root, "modules"),
   361  		},
   362  		Ui: m.Ui,
   363  	}
   364  }
   365  
   366  // process will process the meta-parameters out of the arguments. This
   367  // will potentially modify the args in-place. It will return the resulting
   368  // slice.
   369  //
   370  // vars says whether or not we support variables.
   371  func (m *Meta) process(args []string, vars bool) []string {
   372  	// We do this so that we retain the ability to technically call
   373  	// process multiple times, even if we have no plans to do so
   374  	if m.oldUi != nil {
   375  		m.Ui = m.oldUi
   376  	}
   377  
   378  	// Set colorization
   379  	m.color = m.Color
   380  	for i, v := range args {
   381  		if v == "-no-color" {
   382  			m.color = false
   383  			args = append(args[:i], args[i+1:]...)
   384  			break
   385  		}
   386  	}
   387  
   388  	// Set the UI
   389  	m.oldUi = m.Ui
   390  	m.Ui = &cli.ConcurrentUi{
   391  		Ui: &ColorizeUi{
   392  			Colorize:   m.Colorize(),
   393  			ErrorColor: "[red]",
   394  			Ui:         m.oldUi,
   395  		},
   396  	}
   397  
   398  	// If we support vars and the default var file exists, add it to
   399  	// the args...
   400  	m.autoKey = ""
   401  	if vars {
   402  		if _, err := os.Stat(DefaultVarsFilename); err == nil {
   403  			m.autoKey = "var-file-default"
   404  			args = append(args, "", "")
   405  			copy(args[2:], args[0:])
   406  			args[0] = "-" + m.autoKey
   407  			args[1] = DefaultVarsFilename
   408  		}
   409  	}
   410  
   411  	return args
   412  }
   413  
   414  // uiHook returns the UiHook to use with the context.
   415  func (m *Meta) uiHook() *UiHook {
   416  	return &UiHook{
   417  		Colorize: m.Colorize(),
   418  		Ui:       m.Ui,
   419  	}
   420  }
   421  
   422  // contextOpts are the options used to load a context from a command.
   423  type contextOpts struct {
   424  	// Path to the directory where the root module is.
   425  	Path string
   426  
   427  	// StatePath is the path to the state file. If this is empty, then
   428  	// no state will be loaded. It is also okay for this to be a path to
   429  	// a file that doesn't exist; it is assumed that this means that there
   430  	// is simply no state.
   431  	StatePath string
   432  
   433  	// GetMode is the module.GetMode to use when loading the module tree.
   434  	GetMode module.GetMode
   435  }