package command import ( "bufio" "bytes" "errors" "flag" "fmt" "io" "io/ioutil" "log" "os" "path/filepath" "sort" "strconv" "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/auth" "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 { // 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). 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 // 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 // Credentials provides access to credentials for "terraform-native" // services, which are accessed by a service hostname. Credentials auth.CredentialsSource // 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{} //---------------------------------------------------------- // 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 //---------------------------------------------------------- // Private: do not set these //---------------------------------------------------------- // backendState is the currently active backend state backendState *terraform.BackendState // Variables for the context (private) autoKey string autoVariables map[string]interface{} input bool variables map[string]interface{} // Targets for this context (private) targets []string // Internal fields color bool oldUi cli.Ui // 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. // // 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. // // backupPath is used to backup the state file before writing a modified // version. It defaults to stateOutPath + DefaultBackupExtension // // 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 == "" { 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, } } // DataDir returns the directory where local data will be stored. // Defaults to DefaultDataDir in the current working directory. func (m *Meta) DataDir() string { if m.OverrideDataDir != "" { return m.OverrideDataDir } return DefaultDataDir } 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(), } } // StdinPiped returns true if the input is piped. func (m *Meta) StdinPiped() bool { fi, err := wrappedstreams.Stdin().Stat() if err != nil { // If there is an error, let's just say its not piped return false } return fi.Mode()&os.ModeNamedPipe != 0 } 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 { var opts terraform.ContextOpts opts.Hooks = []terraform.Hook{m.uiHook(), &terraform.DebugHook{}} opts.Hooks = append(opts.Hooks, m.ExtraHooks...) 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 } opts.Variables = vs opts.Targets = m.targets opts.UIInput = m.UIInput() opts.Parallelism = m.parallelism opts.Shadow = m.shadow // 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 } // 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 return f } // 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, m.Credentials) s.Ui = m.Ui s.Mode = mode return s } // 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 m.Color = false args = append(args[:i], args[i+1:]...) break } } // Set the UI m.oldUi = m.Ui m.Ui = &cli.ConcurrentUi{ Ui: &ColorizeUi{ Colorize: m.Colorize(), ErrorColor: "[red]", WarnColor: "[yellow]", 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, } } // 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++ { 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 } // 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 } // 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") }