terraform/command/meta.go

676 lines
19 KiB
Go
Raw Normal View History

package command
import (
"bufio"
"bytes"
"context"
"errors"
2014-07-18 20:37:27 +02:00
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
2014-09-22 20:15:27 +02:00
"path/filepath"
"sort"
"strconv"
2017-01-19 05:50:45 +01:00
"strings"
"time"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/backend/local"
"github.com/hashicorp/terraform/command/format"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/helper/experiment"
"github.com/hashicorp/terraform/helper/variables"
"github.com/hashicorp/terraform/helper/wrappedstreams"
"github.com/hashicorp/terraform/svchost/disco"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
)
// Meta are the meta-options that are available on all or most commands.
type Meta struct {
2017-01-19 05:50:45 +01:00
// The exported fields below should be set by anyone using a
// command with a Meta field. These are expected to be set externally
// (not from within the command itself).
2014-07-28 07:58:35 +02:00
Color bool // True if output should be colored
GlobalPluginDirs []string // Additional paths to search for plugins
PluginOverrides *PluginOverrides // legacy overrides from .terraformrc file
Ui cli.Ui // Ui for output
2017-01-19 05:50:45 +01:00
// ExtraHooks are extra hooks to add to the context.
ExtraHooks []terraform.Hook
// Services provides access to remote endpoint information for
// "terraform-native' services running at a specific user-facing hostname.
Services *disco.Disco
cli: allow disabling "next steps" message in terraform plan In #15884 we adjusted the plan output to give an explicit command to run to apply a plan, whereas before this command was just alluded to in the prose. Since releasing that, we've got good feedback that it's confusing to include such instructions when Terraform is running in a workflow automation tool, because such tools usually abstract away exactly what commands are run and require users to take different actions to proceed through the workflow. To accommodate such environments while retaining helpful messages for normal CLI usage, here we introduce a new environment variable TF_IN_AUTOMATION which, when set to a non-empty value, is a hint to Terraform that it isn't being run in an interactive command shell and it should thus tone down the "next steps" messaging. The documentation for this setting is included as part of the "...in automation" guide since it's not generally useful in other cases. We also intentionally disclaim comprehensive support for this since we want to avoid creating an extreme number of "if running in automation..." codepaths that would increase the testing matrix and hurt maintainability. The focus is specifically on the output of the three commands we give in the automation guide, which at present means the following two situations: * "terraform init" does not include the final paragraphs that suggest running "terraform plan" and tell you in what situations you might need to re-run "terraform init". * "terraform plan" does not include the final paragraphs that either warn about not specifying "-out=..." or instruct to run "terraform apply" with the generated plan file.
2017-09-09 02:14:37 +02:00
// RunningInAutomation indicates that commands are being run by an
// automated system rather than directly at a command prompt.
//
// This is a hint to various command routines that it may be confusing
// to print out messages that suggest running specific follow-up
// commands, since the user consuming the output will not be
// in a position to run such commands.
//
// The intended use-case of this flag is when Terraform is running in
// some sort of workflow orchestration tool which is abstracting away
// the specific commands being run.
RunningInAutomation bool
// PluginCacheDir, if non-empty, enables caching of downloaded plugins
// into the given directory.
PluginCacheDir string
// OverrideDataDir, if non-empty, overrides the return value of the
// DataDir method for situations where the local .terraform/ directory
// is not suitable, e.g. because of a read-only filesystem.
OverrideDataDir string
// When this channel is closed, the command will be cancelled.
ShutdownCh <-chan struct{}
2017-01-19 05:50:45 +01:00
//----------------------------------------------------------
// Protected: commands can set these
//----------------------------------------------------------
// Modify the data directory location. This should be accessed through the
// DataDir method.
dataDir string
// pluginPath is a user defined set of directories to look for plugins.
// This is set during init with the `-plugin-dir` flag, saved to a file in
// the data directory.
// This overrides all other search paths when discovering plugins.
pluginPath []string
ignorePluginChecksum bool
// Override certain behavior for tests within this package
testingOverrides *testingOverrides
2017-01-19 05:50:45 +01:00
//----------------------------------------------------------
// Private: do not set these
//----------------------------------------------------------
// backendState is the currently active backend state
backendState *terraform.BackendState
2014-07-18 20:37:27 +02:00
// Variables for the context (private)
autoKey string
core: Allow lists and maps as variable overrides Terraform 0.7 introduces lists and maps as first-class values for variables, in addition to string values which were previously available. However, there was previously no way to override the default value of a list or map, and the functionality for overriding specific map keys was broken. Using the environment variable method for setting variable values, there was previously no way to give a variable a value of a list or map. These now support HCL for individual values - specifying: TF_VAR_test='["Hello", "World"]' will set the variable `test` to a two-element list containing "Hello" and "World". Specifying TF_VAR_test_map='{"Hello = "World", "Foo" = "bar"}' will set the variable `test_map` to a two-element map with keys "Hello" and "Foo", and values "World" and "bar" respectively. The same logic is applied to `-var` flags, and the file parsed by `-var-files` ("autoVariables"). Note that care must be taken to not run into shell expansion for `-var-` flags and environment variables. We also merge map keys where appropriate. The override syntax has changed (to be noted in CHANGELOG as a breaking change), so several tests needed their syntax updating from the old `amis.us-east-1 = "newValue"` style to `amis = "{ "us-east-1" = "newValue"}"` style as defined in TF-002. In order to continue supporting the `-var "foo=bar"` type of variable flag (which is not valid HCL), a special case error is checked after HCL parsing fails, and the old code path runs instead.
2016-07-21 03:38:26 +02:00
autoVariables map[string]interface{}
input bool
core: Allow lists and maps as variable overrides Terraform 0.7 introduces lists and maps as first-class values for variables, in addition to string values which were previously available. However, there was previously no way to override the default value of a list or map, and the functionality for overriding specific map keys was broken. Using the environment variable method for setting variable values, there was previously no way to give a variable a value of a list or map. These now support HCL for individual values - specifying: TF_VAR_test='["Hello", "World"]' will set the variable `test` to a two-element list containing "Hello" and "World". Specifying TF_VAR_test_map='{"Hello = "World", "Foo" = "bar"}' will set the variable `test_map` to a two-element map with keys "Hello" and "Foo", and values "World" and "bar" respectively. The same logic is applied to `-var` flags, and the file parsed by `-var-files` ("autoVariables"). Note that care must be taken to not run into shell expansion for `-var-` flags and environment variables. We also merge map keys where appropriate. The override syntax has changed (to be noted in CHANGELOG as a breaking change), so several tests needed their syntax updating from the old `amis.us-east-1 = "newValue"` style to `amis = "{ "us-east-1" = "newValue"}"` style as defined in TF-002. In order to continue supporting the `-var "foo=bar"` type of variable flag (which is not valid HCL), a special case error is checked after HCL parsing fails, and the old code path runs instead.
2016-07-21 03:38:26 +02:00
variables map[string]interface{}
2014-07-18 20:37:27 +02:00
// Targets for this context (private)
targets []string
2017-01-19 05:50:45 +01:00
// Internal fields
color bool
oldUi cli.Ui
2015-02-22 03:17:40 +01:00
// The fields below are expected to be set by the command via
// command line flags. See the Apply command for an example.
//
// 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.
2015-02-22 03:17:40 +01:00
//
// 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.
2015-02-22 03:17:40 +01:00
//
// backupPath is used to backup the state file before writing a modified
2015-09-11 20:56:20 +02:00
// version. It defaults to stateOutPath + DefaultBackupExtension
2015-05-06 17:58:42 +02:00
//
// parallelism is used to control the number of concurrent operations
// allowed when walking the graph
//
// shadow is used to enable/disable the shadow graph
//
// provider is to specify specific resource providers
//
// stateLock is set to false to disable state locking
//
// stateLockTimeout is the optional duration to retry a state locks locks
// when it is already locked by another process.
//
// forceInitCopy suppresses confirmation for copying state data during
// init.
//
// reconfigure forces init to ignore any stored configuration.
statePath string
stateOutPath string
backupPath string
parallelism int
shadow bool
provider string
stateLock bool
stateLockTimeout time.Duration
forceInitCopy bool
reconfigure bool
// Used with the import command to allow import of state when no matching config exists.
allowMissingConfig bool
}
type PluginOverrides struct {
Providers map[string]string
Provisioners map[string]string
}
type testingOverrides struct {
ProviderResolver terraform.ResourceProviderResolver
Provisioners map[string]terraform.ResourceProvisionerFactory
}
// 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 == "" {
2015-09-11 20:56:20 +02:00
m.backupPath = m.stateOutPath + DefaultBackupExtension
}
}
// 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,
}
}
2015-03-05 05:52:06 +01:00
// DataDir returns the directory where local data will be stored.
// Defaults to DefaultDataDir in the current working directory.
2015-03-05 05:52:06 +01:00
func (m *Meta) DataDir() string {
if m.OverrideDataDir != "" {
return m.OverrideDataDir
2015-03-05 05:52:06 +01:00
}
return DefaultDataDir
2015-03-05 05:52:06 +01:00
}
const (
// InputModeEnvVar is the environment variable that, if set to "false" or
// "0", causes terraform commands to behave as if the `-input=false` flag was
// specified.
InputModeEnvVar = "TF_INPUT"
)
// 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
}
if envVar := os.Getenv(InputModeEnvVar); envVar != "" {
if v, err := strconv.ParseBool(envVar); err == nil {
if !v {
return 0
}
}
}
var mode terraform.InputMode
mode |= terraform.InputModeProvider
mode |= terraform.InputModeVar
mode |= terraform.InputModeVarUnset
return mode
}
// UIInput returns a UIInput object to be used for asking for input.
func (m *Meta) UIInput() terraform.UIInput {
return &UIInput{
Colorize: m.Colorize(),
}
}
2016-11-14 07:18:18 +01:00
// StdinPiped returns true if the input is piped.
func (m *Meta) StdinPiped() bool {
fi, err := wrappedstreams.Stdin().Stat()
2016-11-14 07:18:18 +01:00
if err != nil {
// If there is an error, let's just say its not piped
return false
}
return fi.Mode()&os.ModeNamedPipe != 0
}
func (m *Meta) RunOperation(b backend.Enhanced, opReq *backend.Operation) (*backend.RunningOperation, error) {
op, err := b.Operation(context.Background(), opReq)
if err != nil {
return nil, fmt.Errorf("error starting operation: %s", err)
}
// Wait for the operation to complete or an interrupt to occur
select {
case <-m.ShutdownCh:
// gracefully stop the operation
op.Stop()
// Notify the user
m.Ui.Output(outputInterrupt)
// Still get the result, since there is still one
select {
case <-m.ShutdownCh:
m.Ui.Error(
"Two interrupts received. Exiting immediately. Note that data\n" +
"loss may have occurred.")
// cancel the operation completely
op.Cancel()
// the operation should return asap
// but timeout just in case
select {
case <-op.Done():
case <-time.After(5 * time.Second):
}
return nil, errors.New("operation canceled")
case <-op.Done():
// operation completed after Stop
}
case <-op.Done():
// operation completed normally
}
if op.Err != nil {
return op, op.Err
}
return op, nil
}
const (
ProviderSkipVerifyEnvVar = "TF_SKIP_PROVIDER_VERIFY"
)
// contextOpts returns the options to use to initialize a Terraform
// context with the settings from this Meta.
func (m *Meta) contextOpts() *terraform.ContextOpts {
2017-01-19 05:50:45 +01:00
var opts terraform.ContextOpts
opts.Hooks = []terraform.Hook{m.uiHook(), &terraform.DebugHook{}}
2017-01-19 05:50:45 +01:00
opts.Hooks = append(opts.Hooks, m.ExtraHooks...)
2014-07-18 20:37:27 +02:00
vs := make(map[string]interface{})
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
}
opts.Variables = vs
opts.Targets = m.targets
2014-10-01 07:01:11 +02:00
opts.UIInput = m.UIInput()
2017-01-19 05:50:45 +01:00
opts.Parallelism = m.parallelism
opts.Shadow = m.shadow
2014-07-18 20:37:27 +02:00
// If testingOverrides are set, we'll skip the plugin discovery process
// and just work with what we've been given, thus allowing the tests
// to provide mock providers and provisioners.
if m.testingOverrides != nil {
opts.ProviderResolver = m.testingOverrides.ProviderResolver
opts.Provisioners = m.testingOverrides.Provisioners
} else {
opts.ProviderResolver = m.providerResolver()
opts.Provisioners = m.provisionerFactories()
}
opts.ProviderSHA256s = m.providerPluginsLock().Read()
if v := os.Getenv(ProviderSkipVerifyEnvVar); v != "" {
opts.SkipProviderVerify = true
}
opts.Meta = &terraform.ContextMeta{
Env: m.Workspace(),
}
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)
f.BoolVar(&m.input, "input", true, "input")
f.Var((*variables.Flag)(&m.variables), "var", "variables")
f.Var((*variables.FlagFile)(&m.variables), "var-file", "variable file")
f.Var((*FlagStringSlice)(&m.targets), "target", "resource to target")
if m.autoKey != "" {
f.Var((*variables.FlagFile)(&m.autoVariables), m.autoKey, "variable file")
}
// Advanced (don't need documentation, or unlikely to be set)
f.BoolVar(&m.shadow, "shadow", true, "shadow graph")
// Experimental features
experiment.Flag(f)
// 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() {
// This only needs to be alive long enough to write the help info if
// there is a flag error. Kill the scanner after a short duriation to
// prevent these from accumulating during tests, and cluttering up the
// stack traces.
time.AfterFunc(2*time.Second, func() {
errW.Close()
})
for errScanner.Scan() {
m.Ui.Error(errScanner.Text())
}
}()
f.SetOutput(errW)
// Set the default Usage to empty
f.Usage = func() {}
// command that bypass locking will supply their own flag on this var, but
// set the initial meta value to true as a failsafe.
m.stateLock = true
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, mode module.GetMode) *module.Storage {
s := module.NewStorage(filepath.Join(root, "modules"), m.Services)
s.Ui = m.Ui
s.Mode = mode
return s
2014-09-22 20:15:27 +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.
//
// vars says whether or not we support variables.
func (m *Meta) process(args []string, vars bool) ([]string, error) {
// 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
2015-06-21 22:51:40 +02:00
m.Color = false
args = append(args[:i], args[i+1:]...)
break
}
}
// 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]",
WarnColor: "[yellow]",
2014-08-19 19:22:26 +02:00
Ui: m.oldUi,
},
}
// If we support vars and the default var file exists, add it to
// the args...
m.autoKey = ""
if vars {
var preArgs []string
if _, err := os.Stat(DefaultVarsFilename); err == nil {
m.autoKey = "var-file-default"
preArgs = append(preArgs, "-"+m.autoKey, DefaultVarsFilename)
}
if _, err := os.Stat(DefaultVarsFilename + ".json"); err == nil {
m.autoKey = "var-file-default"
preArgs = append(preArgs, "-"+m.autoKey, DefaultVarsFilename+".json")
}
wd, err := os.Getwd()
if err != nil {
return nil, err
}
fis, err := ioutil.ReadDir(wd)
if err != nil {
return nil, err
}
// make sure we add the files in order
sort.Slice(fis, func(i, j int) bool {
return fis[i].Name() < fis[j].Name()
})
for _, fi := range fis {
name := fi.Name()
// Ignore directories, non-var-files, and ignored files
if fi.IsDir() || !isAutoVarFile(name) || config.IsIgnoredFile(name) {
continue
}
m.autoKey = "var-file-default"
preArgs = append(preArgs, "-"+m.autoKey, name)
}
args = append(preArgs, args...)
}
return args, nil
}
// uiHook returns the UiHook to use with the context.
func (m *Meta) uiHook() *UiHook {
return &UiHook{
Colorize: m.Colorize(),
Ui: m.Ui,
}
}
2017-01-19 05:50:45 +01:00
// confirm asks a yes/no confirmation.
func (m *Meta) confirm(opts *terraform.InputOpts) (bool, error) {
if !m.Input() {
return false, errors.New("input is disabled")
}
for i := 0; i < 2; i++ {
2017-01-19 05:50:45 +01:00
v, err := m.UIInput().Input(opts)
if err != nil {
return false, fmt.Errorf(
"Error asking for confirmation: %s", err)
}
switch strings.ToLower(v) {
case "no":
return false, nil
case "yes":
return true, nil
}
}
return false, nil
2017-01-19 05:50:45 +01:00
}
// showDiagnostics displays error and warning messages in the UI.
//
// "Diagnostics" here means the Diagnostics type from the tfdiag package,
// though as a convenience this function accepts anything that could be
// passed to the "Append" method on that type, converting it to Diagnostics
// before displaying it.
//
// Internally this function uses Diagnostics.Append, and so it will panic
// if given unsupported value types, just as Append does.
func (m *Meta) showDiagnostics(vals ...interface{}) {
var diags tfdiags.Diagnostics
diags = diags.Append(vals...)
for _, diag := range diags {
// TODO: Actually measure the terminal width and pass it here.
// For now, we don't have easy access to the writer that
// ui.Error (etc) are writing to and thus can't interrogate
// to see if it's a terminal and what size it is.
msg := format.Diagnostic(diag, m.Colorize(), 78)
switch diag.Severity() {
case tfdiags.Error:
m.Ui.Error(msg)
case tfdiags.Warning:
m.Ui.Warn(msg)
default:
m.Ui.Output(msg)
}
}
}
const (
// ModuleDepthDefault is the default value for
// module depth, which can be overridden by flag
// or env var
ModuleDepthDefault = -1
// ModuleDepthEnvVar is the name of the environment variable that can be used to set module depth.
ModuleDepthEnvVar = "TF_MODULE_DEPTH"
)
func (m *Meta) addModuleDepthFlag(flags *flag.FlagSet, moduleDepth *int) {
flags.IntVar(moduleDepth, "module-depth", ModuleDepthDefault, "module-depth")
if envVar := os.Getenv(ModuleDepthEnvVar); envVar != "" {
if md, err := strconv.Atoi(envVar); err == nil {
*moduleDepth = md
}
}
}
// outputShadowError outputs the error from ctx.ShadowError. If the
// error is nil then nothing happens. If output is false then it isn't
// outputted to the user (you can define logic to guard against outputting).
func (m *Meta) outputShadowError(err error, output bool) bool {
// Do nothing if no error
if err == nil {
return false
}
// If not outputting, do nothing
if !output {
return false
}
// Write the shadow error output to a file
path := fmt.Sprintf("terraform-error-%d.log", time.Now().UTC().Unix())
if err := ioutil.WriteFile(path, []byte(err.Error()), 0644); err != nil {
// If there is an error writing it, just let it go
log.Printf("[ERROR] Error writing shadow error: %s", err)
return false
}
// Output!
m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
"[reset][bold][yellow]\nExperimental feature failure! Please report a bug.\n\n"+
"This is not an error. Your Terraform operation completed successfully.\n"+
"Your real infrastructure is unaffected by this message.\n\n"+
"[reset][yellow]While running, Terraform sometimes tests experimental features in the\n"+
"background. These features cannot affect real state and never touch\n"+
"real infrastructure. If the features work properly, you see nothing.\n"+
"If the features fail, this message appears.\n\n"+
"You can report an issue at: https://github.com/hashicorp/terraform/issues\n\n"+
"The failure was written to %q. Please\n"+
"double check this file contains no sensitive information and report\n"+
"it with your issue.\n\n"+
"This is not an error. Your terraform operation completed successfully\n"+
"and your real infrastructure is unaffected by this message.",
path,
)))
return true
}
// WorkspaceNameEnvVar is the name of the environment variable that can be used
// to set the name of the Terraform workspace, overriding the workspace chosen
// by `terraform workspace select`.
//
// Note that this environment variable is ignored by `terraform workspace new`
// and `terraform workspace delete`.
const WorkspaceNameEnvVar = "TF_WORKSPACE"
// Workspace returns the name of the currently configured workspace, corresponding
// to the desired named state.
func (m *Meta) Workspace() string {
current, _ := m.WorkspaceOverridden()
return current
}
// WorkspaceOverridden returns the name of the currently configured workspace,
// corresponding to the desired named state, as well as a bool saying whether
// this was set via the TF_WORKSPACE environment variable.
func (m *Meta) WorkspaceOverridden() (string, bool) {
if envVar := os.Getenv(WorkspaceNameEnvVar); envVar != "" {
return envVar, true
}
envData, err := ioutil.ReadFile(filepath.Join(m.DataDir(), local.DefaultWorkspaceFile))
current := string(bytes.TrimSpace(envData))
if current == "" {
current = backend.DefaultStateName
}
if err != nil && !os.IsNotExist(err) {
// always return the default if we can't get a workspace name
log.Printf("[ERROR] failed to read current workspace: %s", err)
}
return current, false
}
// SetWorkspace saves the given name as the current workspace in the local
// filesystem.
func (m *Meta) SetWorkspace(name string) error {
err := os.MkdirAll(m.DataDir(), 0755)
if err != nil {
return err
}
err = ioutil.WriteFile(filepath.Join(m.DataDir(), local.DefaultWorkspaceFile), []byte(name), 0644)
if err != nil {
return err
}
return nil
}
2017-06-22 03:22:07 +02:00
// isAutoVarFile determines if the file ends with .auto.tfvars or .auto.tfvars.json
func isAutoVarFile(path string) bool {
return strings.HasSuffix(path, ".auto.tfvars") ||
strings.HasSuffix(path, ".auto.tfvars.json")
}