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