opentofu/command/meta.go

346 lines
8.9 KiB
Go
Raw Normal View History

package command
import (
"bufio"
2014-07-18 13:37:27 -05:00
"flag"
"fmt"
"io"
"log"
"os"
2014-09-22 13:15:27 -05:00
"path/filepath"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
)
// Meta are the meta-options that are available on all or most commands.
type Meta struct {
Color bool
ContextOpts *terraform.ContextOpts
Ui cli.Ui
2014-07-28 00:58:35 -05:00
// State read when calling `Context`. This is available after calling
// `Context`.
state state.State
// This can be set by the command itself to provide extra hooks.
extraHooks []terraform.Hook
// This can be set by tests to change some directories
dataDir string
2014-07-18 13:37:27 -05:00
// Variables for the context (private)
autoKey string
autoVariables map[string]string
input bool
variables map[string]string
2014-07-18 13:37:27 -05:00
color bool
oldUi cli.Ui
// useRemoteState is enabled if we are using remote state storage
// This is set when the context is loaded if we read from a remote
// enabled state file.
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
}
}
// StateOutPath returns the true output path for the state file
func (m *Meta) StateOutPath() string {
return m.stateOutPath
}
// Colorize returns the colorization structure for a command.
func (m *Meta) Colorize() *colorstring.Colorize {
return &colorstring.Colorize{
Colors: colorstring.DefaultColors,
Disable: !m.color,
Reset: true,
}
}
// Context returns a Terraform Context taking into account the context
// options used to initialize this meta configuration.
func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
opts := m.contextOpts()
// First try to just read the plan directly from the path given.
f, err := os.Open(copts.Path)
if err == nil {
plan, err := terraform.ReadPlan(f)
f.Close()
if err == nil {
// 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 != nil && plan.State != nil && plan.State.Remote != nil && plan.State.Remote.Type != "" {
log.Printf("[INFO] Enabling remote state from plan")
m.useRemoteState = true
}
if len(m.variables) > 0 {
return nil, false, fmt.Errorf(
"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.")
}
return plan.Context(opts), true, nil
}
}
// Load the statePath if not given
if copts.StatePath != "" {
m.statePath = copts.StatePath
}
2014-07-27 17:09:04 -05:00
// Store the loaded state
state, statePath, err := State(m.statePath)
if err != nil {
return nil, false, err
}
2014-07-28 00:58:35 -05:00
m.state = state
m.stateOutPath = statePath
2014-07-27 17:09:04 -05:00
// Load the root module
mod, err := module.NewTreeModule("", copts.Path)
if err != nil {
return nil, false, fmt.Errorf("Error loading config: %s", err)
}
dataDir := DefaultDataDirectory
if m.dataDir != "" {
dataDir = m.dataDir
}
err = mod.Load(m.moduleStorage(dataDir), copts.GetMode)
2014-09-22 13:15:27 -05:00
if err != nil {
return nil, false, fmt.Errorf("Error downloading modules: %s", err)
}
opts.Module = mod
opts.State = state.State()
ctx := terraform.NewContext(opts)
return ctx, false, nil
}
// 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
}
// State returns the state for this meta.
func (m *Meta) State() (state.State, error) {
if m.state != nil {
return m.state, nil
2014-10-01 00:01:11 -05:00
}
state, _, err := State(m.statePath)
if err != nil {
return nil, err
}
m.state = state
return state, nil
}
// UIInput returns a UIInput object to be used for asking for input.
func (m *Meta) UIInput() terraform.UIInput {
return &UIInput{
Colorize: m.Colorize(),
}
}
// 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 err := m.state.WriteState(s); err != nil {
return err
}
return m.state.PersistState()
}
// Input returns true if we should ask for input for context.
func (m *Meta) Input() bool {
return !test && m.input && len(m.variables) == 0
}
// 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
opts.Hooks = make(
[]terraform.Hook,
len(m.ContextOpts.Hooks)+len(m.extraHooks)+1)
opts.Hooks[0] = m.uiHook()
copy(opts.Hooks[1:], m.ContextOpts.Hooks)
copy(opts.Hooks[len(m.ContextOpts.Hooks)+1:], m.extraHooks)
2014-07-18 13:37:27 -05: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 13:37:27 -05:00
}
opts.Variables = vs
2014-10-01 00:01:11 -05:00
opts.UIInput = m.UIInput()
2014-07-18 13:37:27 -05:00
return &opts
}
2014-07-18 13:37:27 -05:00
// flags adds the meta flags to the given FlagSet.
func (m *Meta) flagSet(n string) *flag.FlagSet {
f := flag.NewFlagSet(n, flag.ContinueOnError)
f.BoolVar(&m.input, "input", true, "input")
2014-07-18 13:37:27 -05:00
f.Var((*FlagVar)(&m.variables), "var", "variables")
f.Var((*FlagVarFile)(&m.variables), "var-file", "variable file")
if m.autoKey != "" {
f.Var((*FlagVarFile)(&m.autoVariables), m.autoKey, "variable file")
}
// 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 13:37:27 -05:00
return f
}
2014-09-22 13:15:27 -05: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,
}
}
// process will process the meta-parameters out of the arguments. This
// will potentially modify the args in-place. It will return the resulting
// slice.
//
// vars says whether or not we support variables.
func (m *Meta) process(args []string, vars bool) []string {
// 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
}
// Set colorization
m.color = m.Color
for i, v := range args {
if v == "-no-color" {
m.color = false
args = append(args[:i], args[i+1:]...)
break
}
}
// Set the UI
m.oldUi = m.Ui
2014-08-19 12:22:26 -05:00
m.Ui = &cli.ConcurrentUi{
Ui: &ColorizeUi{
Colorize: m.Colorize(),
ErrorColor: "[red]",
Ui: m.oldUi,
},
}
// If we support vars and the default var file exists, add it to
// the args...
m.autoKey = ""
if vars {
if _, err := os.Stat(DefaultVarsFilename); err == nil {
m.autoKey = "var-file-default"
args = append(args, "", "")
copy(args[2:], args[0:])
args[0] = "-" + m.autoKey
args[1] = DefaultVarsFilename
}
}
return args
}
// uiHook returns the UiHook to use with the context.
func (m *Meta) uiHook() *UiHook {
return &UiHook{
Colorize: m.Colorize(),
Ui: m.Ui,
}
}
// 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
}