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