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