github.com/jzbruno/terraform@v0.10.3-0.20180104230435-18975d727047/command/meta.go (about) 1 package command 2 3 import ( 4 "bufio" 5 "bytes" 6 "errors" 7 "flag" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "log" 12 "os" 13 "path/filepath" 14 "sort" 15 "strconv" 16 "strings" 17 "time" 18 19 "github.com/hashicorp/terraform/backend" 20 "github.com/hashicorp/terraform/backend/local" 21 "github.com/hashicorp/terraform/command/format" 22 "github.com/hashicorp/terraform/config" 23 "github.com/hashicorp/terraform/config/module" 24 "github.com/hashicorp/terraform/helper/experiment" 25 "github.com/hashicorp/terraform/helper/variables" 26 "github.com/hashicorp/terraform/helper/wrappedstreams" 27 "github.com/hashicorp/terraform/svchost/auth" 28 "github.com/hashicorp/terraform/svchost/disco" 29 "github.com/hashicorp/terraform/terraform" 30 "github.com/hashicorp/terraform/tfdiags" 31 "github.com/mitchellh/cli" 32 "github.com/mitchellh/colorstring" 33 ) 34 35 // Meta are the meta-options that are available on all or most commands. 36 type Meta struct { 37 // The exported fields below should be set by anyone using a 38 // command with a Meta field. These are expected to be set externally 39 // (not from within the command itself). 40 41 Color bool // True if output should be colored 42 GlobalPluginDirs []string // Additional paths to search for plugins 43 PluginOverrides *PluginOverrides // legacy overrides from .terraformrc file 44 Ui cli.Ui // Ui for output 45 46 // ExtraHooks are extra hooks to add to the context. 47 ExtraHooks []terraform.Hook 48 49 // Services provides access to remote endpoint information for 50 // "terraform-native' services running at a specific user-facing hostname. 51 Services *disco.Disco 52 53 // Credentials provides access to credentials for "terraform-native" 54 // services, which are accessed by a service hostname. 55 Credentials auth.CredentialsSource 56 57 // RunningInAutomation indicates that commands are being run by an 58 // automated system rather than directly at a command prompt. 59 // 60 // This is a hint to various command routines that it may be confusing 61 // to print out messages that suggest running specific follow-up 62 // commands, since the user consuming the output will not be 63 // in a position to run such commands. 64 // 65 // The intended use-case of this flag is when Terraform is running in 66 // some sort of workflow orchestration tool which is abstracting away 67 // the specific commands being run. 68 RunningInAutomation bool 69 70 // PluginCacheDir, if non-empty, enables caching of downloaded plugins 71 // into the given directory. 72 PluginCacheDir string 73 74 // OverrideDataDir, if non-empty, overrides the return value of the 75 // DataDir method for situations where the local .terraform/ directory 76 // is not suitable, e.g. because of a read-only filesystem. 77 OverrideDataDir string 78 79 // When this channel is closed, the command will be cancelled. 80 ShutdownCh <-chan struct{} 81 82 //---------------------------------------------------------- 83 // Protected: commands can set these 84 //---------------------------------------------------------- 85 86 // Modify the data directory location. This should be accessed through the 87 // DataDir method. 88 dataDir string 89 90 // pluginPath is a user defined set of directories to look for plugins. 91 // This is set during init with the `-plugin-dir` flag, saved to a file in 92 // the data directory. 93 // This overrides all other search paths when discovering plugins. 94 pluginPath []string 95 96 ignorePluginChecksum bool 97 98 // Override certain behavior for tests within this package 99 testingOverrides *testingOverrides 100 101 //---------------------------------------------------------- 102 // Private: do not set these 103 //---------------------------------------------------------- 104 105 // backendState is the currently active backend state 106 backendState *terraform.BackendState 107 108 // Variables for the context (private) 109 autoKey string 110 autoVariables map[string]interface{} 111 input bool 112 variables map[string]interface{} 113 114 // Targets for this context (private) 115 targets []string 116 117 // Internal fields 118 color bool 119 oldUi cli.Ui 120 121 // The fields below are expected to be set by the command via 122 // command line flags. See the Apply command for an example. 123 // 124 // statePath is the path to the state file. If this is empty, then 125 // no state will be loaded. It is also okay for this to be a path to 126 // a file that doesn't exist; it is assumed that this means that there 127 // is simply no state. 128 // 129 // stateOutPath is used to override the output path for the state. 130 // If not provided, the StatePath is used causing the old state to 131 // be overriden. 132 // 133 // backupPath is used to backup the state file before writing a modified 134 // version. It defaults to stateOutPath + DefaultBackupExtension 135 // 136 // parallelism is used to control the number of concurrent operations 137 // allowed when walking the graph 138 // 139 // shadow is used to enable/disable the shadow graph 140 // 141 // provider is to specify specific resource providers 142 // 143 // stateLock is set to false to disable state locking 144 // 145 // stateLockTimeout is the optional duration to retry a state locks locks 146 // when it is already locked by another process. 147 // 148 // forceInitCopy suppresses confirmation for copying state data during 149 // init. 150 // 151 // reconfigure forces init to ignore any stored configuration. 152 statePath string 153 stateOutPath string 154 backupPath string 155 parallelism int 156 shadow bool 157 provider string 158 stateLock bool 159 stateLockTimeout time.Duration 160 forceInitCopy bool 161 reconfigure bool 162 163 // errWriter is the write side of a pipe for the FlagSet output. We need to 164 // keep track of this to close previous pipes between tests. Normal 165 // operation never needs to close this. 166 errWriter *io.PipeWriter 167 // done chan to wait for the scanner goroutine 168 errScannerDone chan struct{} 169 170 // Used with the import command to allow import of state when no matching config exists. 171 allowMissingConfig bool 172 } 173 174 type PluginOverrides struct { 175 Providers map[string]string 176 Provisioners map[string]string 177 } 178 179 type testingOverrides struct { 180 ProviderResolver terraform.ResourceProviderResolver 181 Provisioners map[string]terraform.ResourceProvisionerFactory 182 } 183 184 // initStatePaths is used to initialize the default values for 185 // statePath, stateOutPath, and backupPath 186 func (m *Meta) initStatePaths() { 187 if m.statePath == "" { 188 m.statePath = DefaultStateFilename 189 } 190 if m.stateOutPath == "" { 191 m.stateOutPath = m.statePath 192 } 193 if m.backupPath == "" { 194 m.backupPath = m.stateOutPath + DefaultBackupExtension 195 } 196 } 197 198 // StateOutPath returns the true output path for the state file 199 func (m *Meta) StateOutPath() string { 200 return m.stateOutPath 201 } 202 203 // Colorize returns the colorization structure for a command. 204 func (m *Meta) Colorize() *colorstring.Colorize { 205 return &colorstring.Colorize{ 206 Colors: colorstring.DefaultColors, 207 Disable: !m.color, 208 Reset: true, 209 } 210 } 211 212 // DataDir returns the directory where local data will be stored. 213 // Defaults to DefaultDataDir in the current working directory. 214 func (m *Meta) DataDir() string { 215 if m.OverrideDataDir != "" { 216 return m.OverrideDataDir 217 } 218 return DefaultDataDir 219 } 220 221 const ( 222 // InputModeEnvVar is the environment variable that, if set to "false" or 223 // "0", causes terraform commands to behave as if the `-input=false` flag was 224 // specified. 225 InputModeEnvVar = "TF_INPUT" 226 ) 227 228 // InputMode returns the type of input we should ask for in the form of 229 // terraform.InputMode which is passed directly to Context.Input. 230 func (m *Meta) InputMode() terraform.InputMode { 231 if test || !m.input { 232 return 0 233 } 234 235 if envVar := os.Getenv(InputModeEnvVar); envVar != "" { 236 if v, err := strconv.ParseBool(envVar); err == nil { 237 if !v { 238 return 0 239 } 240 } 241 } 242 243 var mode terraform.InputMode 244 mode |= terraform.InputModeProvider 245 mode |= terraform.InputModeVar 246 mode |= terraform.InputModeVarUnset 247 248 return mode 249 } 250 251 // UIInput returns a UIInput object to be used for asking for input. 252 func (m *Meta) UIInput() terraform.UIInput { 253 return &UIInput{ 254 Colorize: m.Colorize(), 255 } 256 } 257 258 // StdinPiped returns true if the input is piped. 259 func (m *Meta) StdinPiped() bool { 260 fi, err := wrappedstreams.Stdin().Stat() 261 if err != nil { 262 // If there is an error, let's just say its not piped 263 return false 264 } 265 266 return fi.Mode()&os.ModeNamedPipe != 0 267 } 268 269 const ( 270 ProviderSkipVerifyEnvVar = "TF_SKIP_PROVIDER_VERIFY" 271 ) 272 273 // contextOpts returns the options to use to initialize a Terraform 274 // context with the settings from this Meta. 275 func (m *Meta) contextOpts() *terraform.ContextOpts { 276 var opts terraform.ContextOpts 277 opts.Hooks = []terraform.Hook{m.uiHook(), &terraform.DebugHook{}} 278 opts.Hooks = append(opts.Hooks, m.ExtraHooks...) 279 280 vs := make(map[string]interface{}) 281 for k, v := range opts.Variables { 282 vs[k] = v 283 } 284 for k, v := range m.autoVariables { 285 vs[k] = v 286 } 287 for k, v := range m.variables { 288 vs[k] = v 289 } 290 opts.Variables = vs 291 292 opts.Targets = m.targets 293 opts.UIInput = m.UIInput() 294 opts.Parallelism = m.parallelism 295 opts.Shadow = m.shadow 296 297 // If testingOverrides are set, we'll skip the plugin discovery process 298 // and just work with what we've been given, thus allowing the tests 299 // to provide mock providers and provisioners. 300 if m.testingOverrides != nil { 301 opts.ProviderResolver = m.testingOverrides.ProviderResolver 302 opts.Provisioners = m.testingOverrides.Provisioners 303 } else { 304 opts.ProviderResolver = m.providerResolver() 305 opts.Provisioners = m.provisionerFactories() 306 } 307 308 opts.ProviderSHA256s = m.providerPluginsLock().Read() 309 if v := os.Getenv(ProviderSkipVerifyEnvVar); v != "" { 310 opts.SkipProviderVerify = true 311 } 312 313 opts.Meta = &terraform.ContextMeta{ 314 Env: m.Workspace(), 315 } 316 317 return &opts 318 } 319 320 // flags adds the meta flags to the given FlagSet. 321 func (m *Meta) flagSet(n string) *flag.FlagSet { 322 f := flag.NewFlagSet(n, flag.ContinueOnError) 323 f.BoolVar(&m.input, "input", true, "input") 324 f.Var((*variables.Flag)(&m.variables), "var", "variables") 325 f.Var((*variables.FlagFile)(&m.variables), "var-file", "variable file") 326 f.Var((*FlagStringSlice)(&m.targets), "target", "resource to target") 327 328 if m.autoKey != "" { 329 f.Var((*variables.FlagFile)(&m.autoVariables), m.autoKey, "variable file") 330 } 331 332 // Advanced (don't need documentation, or unlikely to be set) 333 f.BoolVar(&m.shadow, "shadow", true, "shadow graph") 334 335 // Experimental features 336 experiment.Flag(f) 337 338 // Create an io.Writer that writes to our Ui properly for errors. 339 // This is kind of a hack, but it does the job. Basically: create 340 // a pipe, use a scanner to break it into lines, and output each line 341 // to the UI. Do this forever. 342 343 // If a previous pipe exists, we need to close that first. 344 // This should only happen in testing. 345 if m.errWriter != nil { 346 m.errWriter.Close() 347 } 348 349 if m.errScannerDone != nil { 350 <-m.errScannerDone 351 } 352 353 errR, errW := io.Pipe() 354 errScanner := bufio.NewScanner(errR) 355 m.errWriter = errW 356 m.errScannerDone = make(chan struct{}) 357 go func() { 358 defer close(m.errScannerDone) 359 for errScanner.Scan() { 360 m.Ui.Error(errScanner.Text()) 361 } 362 }() 363 f.SetOutput(errW) 364 365 // Set the default Usage to empty 366 f.Usage = func() {} 367 368 // command that bypass locking will supply their own flag on this var, but 369 // set the initial meta value to true as a failsafe. 370 m.stateLock = true 371 372 return f 373 } 374 375 // moduleStorage returns the module.Storage implementation used to store 376 // modules for commands. 377 func (m *Meta) moduleStorage(root string, mode module.GetMode) *module.Storage { 378 s := module.NewStorage(filepath.Join(root, "modules"), m.Services, m.Credentials) 379 s.Ui = m.Ui 380 s.Mode = mode 381 return s 382 } 383 384 // process will process the meta-parameters out of the arguments. This 385 // will potentially modify the args in-place. It will return the resulting 386 // slice. 387 // 388 // vars says whether or not we support variables. 389 func (m *Meta) process(args []string, vars bool) ([]string, error) { 390 // We do this so that we retain the ability to technically call 391 // process multiple times, even if we have no plans to do so 392 if m.oldUi != nil { 393 m.Ui = m.oldUi 394 } 395 396 // Set colorization 397 m.color = m.Color 398 for i, v := range args { 399 if v == "-no-color" { 400 m.color = false 401 m.Color = false 402 args = append(args[:i], args[i+1:]...) 403 break 404 } 405 } 406 407 // Set the UI 408 m.oldUi = m.Ui 409 m.Ui = &cli.ConcurrentUi{ 410 Ui: &ColorizeUi{ 411 Colorize: m.Colorize(), 412 ErrorColor: "[red]", 413 WarnColor: "[yellow]", 414 Ui: m.oldUi, 415 }, 416 } 417 418 // If we support vars and the default var file exists, add it to 419 // the args... 420 m.autoKey = "" 421 if vars { 422 var preArgs []string 423 424 if _, err := os.Stat(DefaultVarsFilename); err == nil { 425 m.autoKey = "var-file-default" 426 preArgs = append(preArgs, "-"+m.autoKey, DefaultVarsFilename) 427 } 428 429 if _, err := os.Stat(DefaultVarsFilename + ".json"); err == nil { 430 m.autoKey = "var-file-default" 431 preArgs = append(preArgs, "-"+m.autoKey, DefaultVarsFilename+".json") 432 } 433 434 wd, err := os.Getwd() 435 if err != nil { 436 return nil, err 437 } 438 439 fis, err := ioutil.ReadDir(wd) 440 if err != nil { 441 return nil, err 442 } 443 444 // make sure we add the files in order 445 sort.Slice(fis, func(i, j int) bool { 446 return fis[i].Name() < fis[j].Name() 447 }) 448 449 for _, fi := range fis { 450 name := fi.Name() 451 // Ignore directories, non-var-files, and ignored files 452 if fi.IsDir() || !isAutoVarFile(name) || config.IsIgnoredFile(name) { 453 continue 454 } 455 456 m.autoKey = "var-file-default" 457 preArgs = append(preArgs, "-"+m.autoKey, name) 458 } 459 460 args = append(preArgs, args...) 461 } 462 463 return args, nil 464 } 465 466 // uiHook returns the UiHook to use with the context. 467 func (m *Meta) uiHook() *UiHook { 468 return &UiHook{ 469 Colorize: m.Colorize(), 470 Ui: m.Ui, 471 } 472 } 473 474 // confirm asks a yes/no confirmation. 475 func (m *Meta) confirm(opts *terraform.InputOpts) (bool, error) { 476 if !m.Input() { 477 return false, errors.New("input is disabled") 478 } 479 480 for i := 0; i < 2; i++ { 481 v, err := m.UIInput().Input(opts) 482 if err != nil { 483 return false, fmt.Errorf( 484 "Error asking for confirmation: %s", err) 485 } 486 487 switch strings.ToLower(v) { 488 case "no": 489 return false, nil 490 case "yes": 491 return true, nil 492 } 493 } 494 return false, nil 495 } 496 497 // showDiagnostics displays error and warning messages in the UI. 498 // 499 // "Diagnostics" here means the Diagnostics type from the tfdiag package, 500 // though as a convenience this function accepts anything that could be 501 // passed to the "Append" method on that type, converting it to Diagnostics 502 // before displaying it. 503 // 504 // Internally this function uses Diagnostics.Append, and so it will panic 505 // if given unsupported value types, just as Append does. 506 func (m *Meta) showDiagnostics(vals ...interface{}) { 507 var diags tfdiags.Diagnostics 508 diags = diags.Append(vals...) 509 510 for _, diag := range diags { 511 // TODO: Actually measure the terminal width and pass it here. 512 // For now, we don't have easy access to the writer that 513 // ui.Error (etc) are writing to and thus can't interrogate 514 // to see if it's a terminal and what size it is. 515 msg := format.Diagnostic(diag, m.Colorize(), 78) 516 switch diag.Severity() { 517 case tfdiags.Error: 518 m.Ui.Error(msg) 519 case tfdiags.Warning: 520 m.Ui.Warn(msg) 521 default: 522 m.Ui.Output(msg) 523 } 524 } 525 } 526 527 const ( 528 // ModuleDepthDefault is the default value for 529 // module depth, which can be overridden by flag 530 // or env var 531 ModuleDepthDefault = -1 532 533 // ModuleDepthEnvVar is the name of the environment variable that can be used to set module depth. 534 ModuleDepthEnvVar = "TF_MODULE_DEPTH" 535 ) 536 537 func (m *Meta) addModuleDepthFlag(flags *flag.FlagSet, moduleDepth *int) { 538 flags.IntVar(moduleDepth, "module-depth", ModuleDepthDefault, "module-depth") 539 if envVar := os.Getenv(ModuleDepthEnvVar); envVar != "" { 540 if md, err := strconv.Atoi(envVar); err == nil { 541 *moduleDepth = md 542 } 543 } 544 } 545 546 // outputShadowError outputs the error from ctx.ShadowError. If the 547 // error is nil then nothing happens. If output is false then it isn't 548 // outputted to the user (you can define logic to guard against outputting). 549 func (m *Meta) outputShadowError(err error, output bool) bool { 550 // Do nothing if no error 551 if err == nil { 552 return false 553 } 554 555 // If not outputting, do nothing 556 if !output { 557 return false 558 } 559 560 // Write the shadow error output to a file 561 path := fmt.Sprintf("terraform-error-%d.log", time.Now().UTC().Unix()) 562 if err := ioutil.WriteFile(path, []byte(err.Error()), 0644); err != nil { 563 // If there is an error writing it, just let it go 564 log.Printf("[ERROR] Error writing shadow error: %s", err) 565 return false 566 } 567 568 // Output! 569 m.Ui.Output(m.Colorize().Color(fmt.Sprintf( 570 "[reset][bold][yellow]\nExperimental feature failure! Please report a bug.\n\n"+ 571 "This is not an error. Your Terraform operation completed successfully.\n"+ 572 "Your real infrastructure is unaffected by this message.\n\n"+ 573 "[reset][yellow]While running, Terraform sometimes tests experimental features in the\n"+ 574 "background. These features cannot affect real state and never touch\n"+ 575 "real infrastructure. If the features work properly, you see nothing.\n"+ 576 "If the features fail, this message appears.\n\n"+ 577 "You can report an issue at: https://github.com/hashicorp/terraform/issues\n\n"+ 578 "The failure was written to %q. Please\n"+ 579 "double check this file contains no sensitive information and report\n"+ 580 "it with your issue.\n\n"+ 581 "This is not an error. Your terraform operation completed successfully\n"+ 582 "and your real infrastructure is unaffected by this message.", 583 path, 584 ))) 585 586 return true 587 } 588 589 // WorkspaceNameEnvVar is the name of the environment variable that can be used 590 // to set the name of the Terraform workspace, overriding the workspace chosen 591 // by `terraform workspace select`. 592 // 593 // Note that this environment variable is ignored by `terraform workspace new` 594 // and `terraform workspace delete`. 595 const WorkspaceNameEnvVar = "TF_WORKSPACE" 596 597 // Workspace returns the name of the currently configured workspace, corresponding 598 // to the desired named state. 599 func (m *Meta) Workspace() string { 600 current, _ := m.WorkspaceOverridden() 601 return current 602 } 603 604 // WorkspaceOverridden returns the name of the currently configured workspace, 605 // corresponding to the desired named state, as well as a bool saying whether 606 // this was set via the TF_WORKSPACE environment variable. 607 func (m *Meta) WorkspaceOverridden() (string, bool) { 608 if envVar := os.Getenv(WorkspaceNameEnvVar); envVar != "" { 609 return envVar, true 610 } 611 612 envData, err := ioutil.ReadFile(filepath.Join(m.DataDir(), local.DefaultWorkspaceFile)) 613 current := string(bytes.TrimSpace(envData)) 614 if current == "" { 615 current = backend.DefaultStateName 616 } 617 618 if err != nil && !os.IsNotExist(err) { 619 // always return the default if we can't get a workspace name 620 log.Printf("[ERROR] failed to read current workspace: %s", err) 621 } 622 623 return current, false 624 } 625 626 // SetWorkspace saves the given name as the current workspace in the local 627 // filesystem. 628 func (m *Meta) SetWorkspace(name string) error { 629 err := os.MkdirAll(m.DataDir(), 0755) 630 if err != nil { 631 return err 632 } 633 634 err = ioutil.WriteFile(filepath.Join(m.DataDir(), local.DefaultWorkspaceFile), []byte(name), 0644) 635 if err != nil { 636 return err 637 } 638 return nil 639 } 640 641 // isAutoVarFile determines if the file ends with .auto.tfvars or .auto.tfvars.json 642 func isAutoVarFile(path string) bool { 643 return strings.HasSuffix(path, ".auto.tfvars") || 644 strings.HasSuffix(path, ".auto.tfvars.json") 645 }