2014-07-13 05:21:46 +02:00
|
|
|
package command
|
|
|
|
|
|
|
|
import (
|
2014-09-09 05:56:18 +02:00
|
|
|
"bufio"
|
2014-07-18 20:37:27 +02:00
|
|
|
"flag"
|
2014-07-13 05:37:30 +02:00
|
|
|
"fmt"
|
2014-09-09 05:56:18 +02:00
|
|
|
"io"
|
2014-10-12 03:05:23 +02:00
|
|
|
"log"
|
2014-07-13 05:37:30 +02:00
|
|
|
"os"
|
2014-09-22 20:15:27 +02:00
|
|
|
"path/filepath"
|
2014-07-13 05:37:30 +02:00
|
|
|
|
2014-09-22 19:56:50 +02:00
|
|
|
"github.com/hashicorp/terraform/config/module"
|
2014-10-12 03:05:23 +02:00
|
|
|
"github.com/hashicorp/terraform/remote"
|
2014-07-13 05:37:30 +02:00
|
|
|
"github.com/hashicorp/terraform/terraform"
|
|
|
|
"github.com/mitchellh/cli"
|
2014-07-13 05:21:46 +02:00
|
|
|
"github.com/mitchellh/colorstring"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Meta are the meta-options that are available on all or most commands.
|
|
|
|
type Meta struct {
|
2014-07-13 05:37:30 +02:00
|
|
|
Color bool
|
|
|
|
ContextOpts *terraform.ContextOpts
|
|
|
|
Ui cli.Ui
|
2014-07-28 07:58:35 +02:00
|
|
|
|
|
|
|
// State read when calling `Context`. This is available after calling
|
|
|
|
// `Context`.
|
|
|
|
state *terraform.State
|
2014-07-13 05:59:16 +02:00
|
|
|
|
2014-07-18 00:14:26 +02:00
|
|
|
// This can be set by the command itself to provide extra hooks.
|
|
|
|
extraHooks []terraform.Hook
|
|
|
|
|
2014-10-13 21:05:28 +02:00
|
|
|
// This can be set by tests to change some directories
|
|
|
|
dataDir string
|
|
|
|
|
2014-07-18 20:37:27 +02:00
|
|
|
// Variables for the context (private)
|
2014-08-25 06:40:58 +02:00
|
|
|
autoKey string
|
|
|
|
autoVariables map[string]string
|
2014-09-29 20:11:35 +02:00
|
|
|
input bool
|
2014-08-25 06:40:58 +02:00
|
|
|
variables map[string]string
|
2014-07-18 20:37:27 +02:00
|
|
|
|
2014-07-17 18:34:32 +02:00
|
|
|
color bool
|
2014-07-13 05:59:16 +02:00
|
|
|
oldUi cli.Ui
|
2014-10-12 03:05:23 +02:00
|
|
|
|
|
|
|
// useRemoteState is enabled if we are using remote state storage
|
2014-10-12 03:21:20 +02:00
|
|
|
// This is set when the context is loaded if we read from a remote
|
|
|
|
// enabled state file.
|
2014-10-12 03:05:23 +02:00
|
|
|
useRemoteState bool
|
|
|
|
|
|
|
|
// statePath is the path to the state file. If this is empty, then
|
|
|
|
// no state will be loaded. It is also okay for this to be a path to
|
|
|
|
// a file that doesn't exist; it is assumed that this means that there
|
|
|
|
// is simply no state.
|
|
|
|
statePath string
|
|
|
|
|
|
|
|
// stateOutPath is used to override the output path for the state.
|
|
|
|
// If not provided, the StatePath is used causing the old state to
|
|
|
|
// be overriden.
|
|
|
|
stateOutPath string
|
|
|
|
|
|
|
|
// backupPath is used to backup the state file before writing a modified
|
|
|
|
// version. It defaults to stateOutPath + DefaultBackupExtention
|
|
|
|
backupPath string
|
|
|
|
}
|
|
|
|
|
|
|
|
// initStatePaths is used to initialize the default values for
|
|
|
|
// statePath, stateOutPath, and backupPath
|
|
|
|
func (m *Meta) initStatePaths() {
|
|
|
|
if m.statePath == "" {
|
|
|
|
m.statePath = DefaultStateFilename
|
|
|
|
}
|
|
|
|
if m.stateOutPath == "" {
|
|
|
|
m.stateOutPath = m.statePath
|
|
|
|
}
|
|
|
|
if m.backupPath == "" {
|
|
|
|
m.backupPath = m.stateOutPath + DefaultBackupExtention
|
|
|
|
}
|
2014-07-13 05:21:46 +02:00
|
|
|
}
|
|
|
|
|
2014-10-12 03:34:11 +02:00
|
|
|
// StateOutPath returns the true output path for the state file
|
|
|
|
func (m *Meta) StateOutPath() string {
|
|
|
|
m.initStatePaths()
|
|
|
|
if m.useRemoteState {
|
|
|
|
path, _ := remote.HiddenStatePath()
|
|
|
|
return path
|
|
|
|
}
|
|
|
|
return m.stateOutPath
|
|
|
|
}
|
|
|
|
|
2014-07-13 05:21:46 +02:00
|
|
|
// Colorize returns the colorization structure for a command.
|
|
|
|
func (m *Meta) Colorize() *colorstring.Colorize {
|
|
|
|
return &colorstring.Colorize{
|
|
|
|
Colors: colorstring.DefaultColors,
|
2014-07-17 18:34:32 +02:00
|
|
|
Disable: !m.color,
|
2014-07-13 05:21:46 +02:00
|
|
|
Reset: true,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-07-13 05:37:30 +02:00
|
|
|
// Context returns a Terraform Context taking into account the context
|
|
|
|
// options used to initialize this meta configuration.
|
2014-09-22 19:56:50 +02:00
|
|
|
func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
|
2014-07-13 05:37:30 +02:00
|
|
|
opts := m.contextOpts()
|
|
|
|
|
|
|
|
// First try to just read the plan directly from the path given.
|
2014-09-22 19:56:50 +02:00
|
|
|
f, err := os.Open(copts.Path)
|
2014-07-13 05:37:30 +02:00
|
|
|
if err == nil {
|
|
|
|
plan, err := terraform.ReadPlan(f)
|
|
|
|
f.Close()
|
|
|
|
if err == nil {
|
2015-01-07 22:08:32 +01:00
|
|
|
// Check if remote state is enabled, but do not refresh.
|
|
|
|
// Since a plan is supposed to lock-in the changes, we do not
|
|
|
|
// attempt a state refresh.
|
|
|
|
if plan.State.Remote != nil && plan.State.Remote.Type != "" {
|
|
|
|
log.Printf("[INFO] Enabling remote state from plan")
|
|
|
|
m.useRemoteState = true
|
|
|
|
}
|
|
|
|
|
2014-07-26 23:32:09 +02:00
|
|
|
if len(m.variables) > 0 {
|
2014-07-27 02:45:38 +02:00
|
|
|
return nil, false, fmt.Errorf(
|
2014-07-26 23:32:09 +02:00
|
|
|
"You can't set variables with the '-var' or '-var-file' flag\n" +
|
|
|
|
"when you're applying a plan file. The variables used when\n" +
|
|
|
|
"the plan was created will be used. If you wish to use different\n" +
|
|
|
|
"variable values, create a new plan file.")
|
|
|
|
}
|
|
|
|
|
2014-07-27 02:45:38 +02:00
|
|
|
return plan.Context(opts), true, nil
|
2014-07-13 05:37:30 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-10-12 03:21:20 +02:00
|
|
|
// Load the statePath if not given
|
2014-09-22 19:56:50 +02:00
|
|
|
if copts.StatePath != "" {
|
2014-10-12 03:21:20 +02:00
|
|
|
m.statePath = copts.StatePath
|
2014-07-13 05:37:30 +02:00
|
|
|
}
|
|
|
|
|
2014-07-28 00:09:04 +02:00
|
|
|
// Store the loaded state
|
2014-10-12 03:21:20 +02:00
|
|
|
state, err := m.loadState()
|
|
|
|
if err != nil {
|
|
|
|
return nil, false, err
|
|
|
|
}
|
2014-07-28 07:58:35 +02:00
|
|
|
m.state = state
|
2014-07-28 00:09:04 +02:00
|
|
|
|
2014-09-22 19:56:50 +02:00
|
|
|
// Load the root module
|
|
|
|
mod, err := module.NewTreeModule("", copts.Path)
|
2014-07-13 05:37:30 +02:00
|
|
|
if err != nil {
|
2014-07-27 02:45:38 +02:00
|
|
|
return nil, false, fmt.Errorf("Error loading config: %s", err)
|
2014-07-13 05:37:30 +02:00
|
|
|
}
|
2014-10-13 21:05:28 +02:00
|
|
|
|
|
|
|
dataDir := DefaultDataDirectory
|
|
|
|
if m.dataDir != "" {
|
|
|
|
dataDir = m.dataDir
|
|
|
|
}
|
|
|
|
err = mod.Load(m.moduleStorage(dataDir), copts.GetMode)
|
2014-09-22 20:15:27 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, false, fmt.Errorf("Error downloading modules: %s", err)
|
|
|
|
}
|
2014-07-13 05:37:30 +02:00
|
|
|
|
2014-09-24 23:37:24 +02:00
|
|
|
opts.Module = mod
|
2014-07-13 05:37:30 +02:00
|
|
|
opts.State = state
|
|
|
|
ctx := terraform.NewContext(opts)
|
2014-07-27 02:45:38 +02:00
|
|
|
return ctx, false, nil
|
2014-07-13 05:37:30 +02:00
|
|
|
}
|
|
|
|
|
2014-10-08 19:29:54 +02:00
|
|
|
// InputMode returns the type of input we should ask for in the form of
|
|
|
|
// terraform.InputMode which is passed directly to Context.Input.
|
|
|
|
func (m *Meta) InputMode() terraform.InputMode {
|
|
|
|
if test || !m.input {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
var mode terraform.InputMode
|
|
|
|
mode |= terraform.InputModeProvider
|
|
|
|
if len(m.variables) == 0 && m.autoKey == "" {
|
|
|
|
mode |= terraform.InputModeVar
|
|
|
|
}
|
|
|
|
|
|
|
|
return mode
|
2014-09-29 20:11:35 +02:00
|
|
|
}
|
|
|
|
|
2014-10-01 07:01:11 +02:00
|
|
|
// UIInput returns a UIInput object to be used for asking for input.
|
|
|
|
func (m *Meta) UIInput() terraform.UIInput {
|
|
|
|
return &UIInput{
|
|
|
|
Colorize: m.Colorize(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-10-12 03:21:20 +02:00
|
|
|
// laodState is used to load the Terraform state. We give precedence
|
|
|
|
// to a remote state if enabled, and then check the normal state path.
|
|
|
|
func (m *Meta) loadState() (*terraform.State, error) {
|
|
|
|
// Check if we remote state is enabled
|
|
|
|
localCache, _, err := remote.ReadLocalState()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Error loading state: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set the state if enabled
|
|
|
|
var state *terraform.State
|
|
|
|
if localCache != nil {
|
2014-10-12 03:57:12 +02:00
|
|
|
// Refresh the state
|
|
|
|
log.Printf("[INFO] Refreshing local state...")
|
|
|
|
changes, err := remote.RefreshState(localCache.Remote)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Failed to refresh state: %v", err)
|
|
|
|
}
|
|
|
|
switch changes {
|
|
|
|
case remote.StateChangeNoop:
|
|
|
|
case remote.StateChangeInit:
|
|
|
|
case remote.StateChangeLocalNewer:
|
|
|
|
case remote.StateChangeUpdateLocal:
|
|
|
|
// Reload the state since we've udpated
|
|
|
|
localCache, _, err = remote.ReadLocalState()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Error loading state: %s", err)
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("%s", changes)
|
|
|
|
}
|
|
|
|
|
2014-10-12 03:21:20 +02:00
|
|
|
state = localCache
|
|
|
|
m.useRemoteState = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load up the state
|
|
|
|
if m.statePath != "" {
|
|
|
|
f, err := os.Open(m.statePath)
|
|
|
|
if err != nil && os.IsNotExist(err) {
|
|
|
|
// If the state file doesn't exist, it is okay, since it
|
|
|
|
// is probably a new infrastructure.
|
|
|
|
err = nil
|
|
|
|
} else if m.useRemoteState && err == nil {
|
|
|
|
err = fmt.Errorf("Remote state enabled, but state file '%s' also present.", m.statePath)
|
|
|
|
f.Close()
|
|
|
|
} else if err == nil {
|
|
|
|
state, err = terraform.ReadState(f)
|
|
|
|
f.Close()
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Error loading state: %s", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return state, nil
|
|
|
|
}
|
|
|
|
|
2014-10-12 03:05:23 +02:00
|
|
|
// PersistState is used to write out the state, handling backup of
|
|
|
|
// the existing state file and respecting path configurations.
|
|
|
|
func (m *Meta) PersistState(s *terraform.State) error {
|
|
|
|
if m.useRemoteState {
|
|
|
|
return m.persistRemoteState(s)
|
|
|
|
}
|
|
|
|
return m.persistLocalState(s)
|
|
|
|
}
|
|
|
|
|
|
|
|
// persistRemoteState is used to handle persisting a state file
|
|
|
|
// when remote state management is enabled
|
|
|
|
func (m *Meta) persistRemoteState(s *terraform.State) error {
|
|
|
|
log.Printf("[INFO] Persisting state to local cache")
|
|
|
|
if err := remote.PersistState(s); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
log.Printf("[INFO] Uploading state to remote store")
|
|
|
|
change, err := remote.PushState(s.Remote, false)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if !change.SuccessfulPush() {
|
|
|
|
return fmt.Errorf("Failed to upload state: %s", change)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// persistLocalState is used to handle persisting a state file
|
|
|
|
// when remote state management is disabled.
|
|
|
|
func (m *Meta) persistLocalState(s *terraform.State) error {
|
|
|
|
m.initStatePaths()
|
|
|
|
|
|
|
|
// Create a backup of the state before updating
|
|
|
|
if m.backupPath != "-" {
|
|
|
|
log.Printf("[INFO] Writing backup state to: %s", m.backupPath)
|
|
|
|
if err := remote.CopyFile(m.statePath, m.backupPath); err != nil {
|
|
|
|
return fmt.Errorf("Failed to backup state: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Open the new state file
|
|
|
|
fh, err := os.Create(m.stateOutPath)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Failed to open state file: %v", err)
|
|
|
|
}
|
|
|
|
defer fh.Close()
|
|
|
|
|
|
|
|
// Write out the state
|
|
|
|
if err := terraform.WriteState(s, fh); err != nil {
|
|
|
|
return fmt.Errorf("Failed to encode the state: %v", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Input returns true if we should ask for input for context.
|
|
|
|
func (m *Meta) Input() bool {
|
|
|
|
return !test && m.input && len(m.variables) == 0
|
|
|
|
}
|
|
|
|
|
2014-07-13 05:37:30 +02:00
|
|
|
// contextOpts returns the options to use to initialize a Terraform
|
|
|
|
// context with the settings from this Meta.
|
|
|
|
func (m *Meta) contextOpts() *terraform.ContextOpts {
|
|
|
|
var opts terraform.ContextOpts = *m.ContextOpts
|
2014-07-18 00:14:26 +02:00
|
|
|
opts.Hooks = make(
|
|
|
|
[]terraform.Hook,
|
|
|
|
len(m.ContextOpts.Hooks)+len(m.extraHooks)+1)
|
2014-07-13 05:37:30 +02:00
|
|
|
opts.Hooks[0] = m.uiHook()
|
|
|
|
copy(opts.Hooks[1:], m.ContextOpts.Hooks)
|
2014-07-18 00:14:26 +02:00
|
|
|
copy(opts.Hooks[len(m.ContextOpts.Hooks)+1:], m.extraHooks)
|
2014-07-18 20:37:27 +02:00
|
|
|
|
2014-08-25 06:40:58 +02:00
|
|
|
vs := make(map[string]string)
|
|
|
|
for k, v := range opts.Variables {
|
|
|
|
vs[k] = v
|
|
|
|
}
|
|
|
|
for k, v := range m.autoVariables {
|
|
|
|
vs[k] = v
|
|
|
|
}
|
|
|
|
for k, v := range m.variables {
|
|
|
|
vs[k] = v
|
2014-07-18 20:37:27 +02:00
|
|
|
}
|
2014-08-25 06:40:58 +02:00
|
|
|
opts.Variables = vs
|
2014-10-01 07:01:11 +02:00
|
|
|
opts.UIInput = m.UIInput()
|
2014-07-18 20:37:27 +02:00
|
|
|
|
2014-07-13 05:37:30 +02:00
|
|
|
return &opts
|
|
|
|
}
|
|
|
|
|
2014-07-18 20:37:27 +02:00
|
|
|
// flags adds the meta flags to the given FlagSet.
|
|
|
|
func (m *Meta) flagSet(n string) *flag.FlagSet {
|
|
|
|
f := flag.NewFlagSet(n, flag.ContinueOnError)
|
2014-09-29 20:11:35 +02:00
|
|
|
f.BoolVar(&m.input, "input", true, "input")
|
2014-07-18 20:37:27 +02:00
|
|
|
f.Var((*FlagVar)(&m.variables), "var", "variables")
|
|
|
|
f.Var((*FlagVarFile)(&m.variables), "var-file", "variable file")
|
2014-08-25 06:40:58 +02:00
|
|
|
|
|
|
|
if m.autoKey != "" {
|
|
|
|
f.Var((*FlagVarFile)(&m.autoVariables), m.autoKey, "variable file")
|
|
|
|
}
|
|
|
|
|
2014-09-09 05:56:18 +02:00
|
|
|
// Create an io.Writer that writes to our Ui properly for errors.
|
|
|
|
// This is kind of a hack, but it does the job. Basically: create
|
|
|
|
// a pipe, use a scanner to break it into lines, and output each line
|
|
|
|
// to the UI. Do this forever.
|
|
|
|
errR, errW := io.Pipe()
|
|
|
|
errScanner := bufio.NewScanner(errR)
|
|
|
|
go func() {
|
|
|
|
for errScanner.Scan() {
|
|
|
|
m.Ui.Error(errScanner.Text())
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
f.SetOutput(errW)
|
|
|
|
|
2014-07-18 20:37:27 +02:00
|
|
|
return f
|
|
|
|
}
|
|
|
|
|
2014-09-22 20:15:27 +02:00
|
|
|
// moduleStorage returns the module.Storage implementation used to store
|
|
|
|
// modules for commands.
|
|
|
|
func (m *Meta) moduleStorage(root string) module.Storage {
|
|
|
|
return &uiModuleStorage{
|
|
|
|
Storage: &module.FolderStorage{
|
|
|
|
StorageDir: filepath.Join(root, "modules"),
|
|
|
|
},
|
|
|
|
Ui: m.Ui,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-07-13 05:21:46 +02:00
|
|
|
// process will process the meta-parameters out of the arguments. This
|
|
|
|
// will potentially modify the args in-place. It will return the resulting
|
|
|
|
// slice.
|
2014-10-01 17:37:57 +02:00
|
|
|
//
|
|
|
|
// vars says whether or not we support variables.
|
2014-08-05 18:32:01 +02:00
|
|
|
func (m *Meta) process(args []string, vars bool) []string {
|
2014-07-13 05:59:16 +02:00
|
|
|
// We do this so that we retain the ability to technically call
|
|
|
|
// process multiple times, even if we have no plans to do so
|
|
|
|
if m.oldUi != nil {
|
|
|
|
m.Ui = m.oldUi
|
|
|
|
}
|
2014-07-13 05:21:46 +02:00
|
|
|
|
2014-07-13 05:59:16 +02:00
|
|
|
// Set colorization
|
2014-07-17 18:34:32 +02:00
|
|
|
m.color = m.Color
|
2014-07-13 05:21:46 +02:00
|
|
|
for i, v := range args {
|
|
|
|
if v == "-no-color" {
|
2014-09-09 05:41:10 +02:00
|
|
|
m.color = false
|
2014-07-13 05:59:16 +02:00
|
|
|
args = append(args[:i], args[i+1:]...)
|
|
|
|
break
|
2014-07-13 05:21:46 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-07-13 05:59:16 +02:00
|
|
|
// Set the UI
|
|
|
|
m.oldUi = m.Ui
|
2014-08-19 19:22:26 +02:00
|
|
|
m.Ui = &cli.ConcurrentUi{
|
|
|
|
Ui: &ColorizeUi{
|
|
|
|
Colorize: m.Colorize(),
|
|
|
|
ErrorColor: "[red]",
|
|
|
|
Ui: m.oldUi,
|
|
|
|
},
|
2014-07-13 05:59:16 +02:00
|
|
|
}
|
|
|
|
|
2014-08-05 18:32:01 +02:00
|
|
|
// If we support vars and the default var file exists, add it to
|
|
|
|
// the args...
|
2014-08-25 06:40:58 +02:00
|
|
|
m.autoKey = ""
|
2014-08-05 18:32:01 +02:00
|
|
|
if vars {
|
|
|
|
if _, err := os.Stat(DefaultVarsFilename); err == nil {
|
2014-09-09 05:57:08 +02:00
|
|
|
m.autoKey = "var-file-default"
|
2014-08-05 18:32:01 +02:00
|
|
|
args = append(args, "", "")
|
|
|
|
copy(args[2:], args[0:])
|
2014-08-25 06:40:58 +02:00
|
|
|
args[0] = "-" + m.autoKey
|
2014-08-05 18:32:01 +02:00
|
|
|
args[1] = DefaultVarsFilename
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-07-13 05:21:46 +02:00
|
|
|
return args
|
|
|
|
}
|
2014-07-13 05:37:30 +02:00
|
|
|
|
|
|
|
// uiHook returns the UiHook to use with the context.
|
|
|
|
func (m *Meta) uiHook() *UiHook {
|
|
|
|
return &UiHook{
|
|
|
|
Colorize: m.Colorize(),
|
|
|
|
Ui: m.Ui,
|
|
|
|
}
|
|
|
|
}
|
2014-09-22 19:56:50 +02:00
|
|
|
|
|
|
|
// contextOpts are the options used to load a context from a command.
|
|
|
|
type contextOpts struct {
|
|
|
|
// Path to the directory where the root module is.
|
|
|
|
Path string
|
|
|
|
|
|
|
|
// StatePath is the path to the state file. If this is empty, then
|
|
|
|
// no state will be loaded. It is also okay for this to be a path to
|
|
|
|
// a file that doesn't exist; it is assumed that this means that there
|
|
|
|
// is simply no state.
|
|
|
|
StatePath string
|
|
|
|
|
|
|
|
// GetMode is the module.GetMode to use when loading the module tree.
|
|
|
|
GetMode module.GetMode
|
|
|
|
}
|