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