github.com/aspring/terraform@v0.8.2-0.20161216122603-6a8619a5db2e/command/meta.go (about) 1 package command 2 3 import ( 4 "bufio" 5 "flag" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "log" 10 "os" 11 "path/filepath" 12 "strconv" 13 "time" 14 15 "github.com/hashicorp/errwrap" 16 "github.com/hashicorp/go-getter" 17 "github.com/hashicorp/terraform/config" 18 "github.com/hashicorp/terraform/config/module" 19 "github.com/hashicorp/terraform/helper/experiment" 20 "github.com/hashicorp/terraform/helper/variables" 21 "github.com/hashicorp/terraform/helper/wrappedstreams" 22 "github.com/hashicorp/terraform/state" 23 "github.com/hashicorp/terraform/terraform" 24 "github.com/mitchellh/cli" 25 "github.com/mitchellh/colorstring" 26 ) 27 28 // Meta are the meta-options that are available on all or most commands. 29 type Meta struct { 30 Color bool 31 ContextOpts *terraform.ContextOpts 32 Ui cli.Ui 33 34 // State read when calling `Context`. This is available after calling 35 // `Context`. 36 state state.State 37 stateResult *StateResult 38 39 // This can be set by the command itself to provide extra hooks. 40 extraHooks []terraform.Hook 41 42 // This can be set by tests to change some directories 43 dataDir string 44 45 // Variables for the context (private) 46 autoKey string 47 autoVariables map[string]interface{} 48 input bool 49 variables map[string]interface{} 50 51 // Targets for this context (private) 52 targets []string 53 54 color bool 55 oldUi cli.Ui 56 57 // The fields below are expected to be set by the command via 58 // command line flags. See the Apply command for an example. 59 // 60 // statePath is the path to the state file. If this is empty, then 61 // no state will be loaded. It is also okay for this to be a path to 62 // a file that doesn't exist; it is assumed that this means that there 63 // is simply no state. 64 // 65 // stateOutPath is used to override the output path for the state. 66 // If not provided, the StatePath is used causing the old state to 67 // be overriden. 68 // 69 // backupPath is used to backup the state file before writing a modified 70 // version. It defaults to stateOutPath + DefaultBackupExtension 71 // 72 // parallelism is used to control the number of concurrent operations 73 // allowed when walking the graph 74 // 75 // shadow is used to enable/disable the shadow graph 76 // 77 // provider is to specify specific resource providers 78 statePath string 79 stateOutPath string 80 backupPath string 81 parallelism int 82 shadow bool 83 provider string 84 } 85 86 // initStatePaths is used to initialize the default values for 87 // statePath, stateOutPath, and backupPath 88 func (m *Meta) initStatePaths() { 89 if m.statePath == "" { 90 m.statePath = DefaultStateFilename 91 } 92 if m.stateOutPath == "" { 93 m.stateOutPath = m.statePath 94 } 95 if m.backupPath == "" { 96 m.backupPath = m.stateOutPath + DefaultBackupExtension 97 } 98 } 99 100 // StateOutPath returns the true output path for the state file 101 func (m *Meta) StateOutPath() string { 102 return m.stateOutPath 103 } 104 105 // Colorize returns the colorization structure for a command. 106 func (m *Meta) Colorize() *colorstring.Colorize { 107 return &colorstring.Colorize{ 108 Colors: colorstring.DefaultColors, 109 Disable: !m.color, 110 Reset: true, 111 } 112 } 113 114 // Context returns a Terraform Context taking into account the context 115 // options used to initialize this meta configuration. 116 func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) { 117 opts := m.contextOpts() 118 119 // First try to just read the plan directly from the path given. 120 f, err := os.Open(copts.Path) 121 if err == nil { 122 plan, err := terraform.ReadPlan(f) 123 f.Close() 124 if err == nil { 125 // Setup our state, force it to use our plan's state 126 stateOpts := m.StateOpts() 127 if plan != nil { 128 stateOpts.ForceState = plan.State 129 } 130 131 // Get the state 132 result, err := State(stateOpts) 133 if err != nil { 134 return nil, false, fmt.Errorf("Error loading plan: %s", err) 135 } 136 137 // Set our state 138 m.state = result.State 139 140 // this is used for printing the saved location later 141 if m.stateOutPath == "" { 142 m.stateOutPath = result.StatePath 143 } 144 145 if len(m.variables) > 0 { 146 return nil, false, fmt.Errorf( 147 "You can't set variables with the '-var' or '-var-file' flag\n" + 148 "when you're applying a plan file. The variables used when\n" + 149 "the plan was created will be used. If you wish to use different\n" + 150 "variable values, create a new plan file.") 151 } 152 153 ctx, err := plan.Context(opts) 154 return ctx, true, err 155 } 156 } 157 158 // Load the statePath if not given 159 if copts.StatePath != "" { 160 m.statePath = copts.StatePath 161 } 162 163 // Tell the context if we're in a destroy plan / apply 164 opts.Destroy = copts.Destroy 165 166 // Store the loaded state 167 state, err := m.State() 168 if err != nil { 169 return nil, false, err 170 } 171 172 // Load the root module 173 var mod *module.Tree 174 if copts.Path != "" { 175 mod, err = module.NewTreeModule("", copts.Path) 176 177 // Check for the error where we have no config files but 178 // allow that. If that happens, clear the error. 179 if errwrap.ContainsType(err, new(config.ErrNoConfigsFound)) && 180 copts.PathEmptyOk { 181 log.Printf( 182 "[WARN] Empty configuration dir, ignoring: %s", copts.Path) 183 err = nil 184 mod = module.NewEmptyTree() 185 } 186 187 if err != nil { 188 return nil, false, fmt.Errorf("Error loading config: %s", err) 189 } 190 } else { 191 mod = module.NewEmptyTree() 192 } 193 194 err = mod.Load(m.moduleStorage(m.DataDir()), copts.GetMode) 195 if err != nil { 196 return nil, false, fmt.Errorf("Error downloading modules: %s", err) 197 } 198 199 // Validate the module right away 200 if err := mod.Validate(); err != nil { 201 return nil, false, err 202 } 203 204 opts.Module = mod 205 opts.Parallelism = copts.Parallelism 206 opts.State = state.State() 207 ctx, err := terraform.NewContext(opts) 208 return ctx, false, err 209 } 210 211 // DataDir returns the directory where local data will be stored. 212 func (m *Meta) DataDir() string { 213 dataDir := DefaultDataDir 214 if m.dataDir != "" { 215 dataDir = m.dataDir 216 } 217 218 return dataDir 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 // State returns the state for this meta. 252 func (m *Meta) State() (state.State, error) { 253 if m.state != nil { 254 return m.state, nil 255 } 256 257 result, err := State(m.StateOpts()) 258 if err != nil { 259 return nil, err 260 } 261 262 m.state = result.State 263 m.stateOutPath = result.StatePath 264 m.stateResult = result 265 return m.state, nil 266 } 267 268 // StateRaw is used to setup the state manually. 269 func (m *Meta) StateRaw(opts *StateOpts) (*StateResult, error) { 270 result, err := State(opts) 271 if err != nil { 272 return nil, err 273 } 274 275 m.state = result.State 276 m.stateOutPath = result.StatePath 277 m.stateResult = result 278 return result, nil 279 } 280 281 // StateOpts returns the default state options 282 func (m *Meta) StateOpts() *StateOpts { 283 localPath := m.statePath 284 if localPath == "" { 285 localPath = DefaultStateFilename 286 } 287 remotePath := filepath.Join(m.DataDir(), DefaultStateFilename) 288 289 return &StateOpts{ 290 LocalPath: localPath, 291 LocalPathOut: m.stateOutPath, 292 RemotePath: remotePath, 293 RemoteRefresh: true, 294 BackupPath: m.backupPath, 295 } 296 } 297 298 // UIInput returns a UIInput object to be used for asking for input. 299 func (m *Meta) UIInput() terraform.UIInput { 300 return &UIInput{ 301 Colorize: m.Colorize(), 302 } 303 } 304 305 // PersistState is used to write out the state, handling backup of 306 // the existing state file and respecting path configurations. 307 func (m *Meta) PersistState(s *terraform.State) error { 308 if err := m.state.WriteState(s); err != nil { 309 return err 310 } 311 312 return m.state.PersistState() 313 } 314 315 // Input returns true if we should ask for input for context. 316 func (m *Meta) Input() bool { 317 return !test && m.input && len(m.variables) == 0 318 } 319 320 // StdinPiped returns true if the input is piped. 321 func (m *Meta) StdinPiped() bool { 322 fi, err := wrappedstreams.Stdin().Stat() 323 if err != nil { 324 // If there is an error, let's just say its not piped 325 return false 326 } 327 328 return fi.Mode()&os.ModeNamedPipe != 0 329 } 330 331 // contextOpts returns the options to use to initialize a Terraform 332 // context with the settings from this Meta. 333 func (m *Meta) contextOpts() *terraform.ContextOpts { 334 var opts terraform.ContextOpts = *m.ContextOpts 335 336 opts.Hooks = []terraform.Hook{m.uiHook(), &terraform.DebugHook{}} 337 opts.Hooks = append(opts.Hooks, m.ContextOpts.Hooks...) 338 opts.Hooks = append(opts.Hooks, m.extraHooks...) 339 340 vs := make(map[string]interface{}) 341 for k, v := range opts.Variables { 342 vs[k] = v 343 } 344 for k, v := range m.autoVariables { 345 vs[k] = v 346 } 347 for k, v := range m.variables { 348 vs[k] = v 349 } 350 opts.Variables = vs 351 opts.Targets = m.targets 352 opts.UIInput = m.UIInput() 353 opts.Shadow = m.shadow 354 355 return &opts 356 } 357 358 // flags adds the meta flags to the given FlagSet. 359 func (m *Meta) flagSet(n string) *flag.FlagSet { 360 f := flag.NewFlagSet(n, flag.ContinueOnError) 361 f.BoolVar(&m.input, "input", true, "input") 362 f.Var((*variables.Flag)(&m.variables), "var", "variables") 363 f.Var((*variables.FlagFile)(&m.variables), "var-file", "variable file") 364 f.Var((*FlagStringSlice)(&m.targets), "target", "resource to target") 365 366 if m.autoKey != "" { 367 f.Var((*variables.FlagFile)(&m.autoVariables), m.autoKey, "variable file") 368 } 369 370 // Advanced (don't need documentation, or unlikely to be set) 371 f.BoolVar(&m.shadow, "shadow", true, "shadow graph") 372 373 // Experimental features 374 experiment.Flag(f) 375 376 // Create an io.Writer that writes to our Ui properly for errors. 377 // This is kind of a hack, but it does the job. Basically: create 378 // a pipe, use a scanner to break it into lines, and output each line 379 // to the UI. Do this forever. 380 errR, errW := io.Pipe() 381 errScanner := bufio.NewScanner(errR) 382 go func() { 383 for errScanner.Scan() { 384 m.Ui.Error(errScanner.Text()) 385 } 386 }() 387 f.SetOutput(errW) 388 389 // Set the default Usage to empty 390 f.Usage = func() {} 391 392 return f 393 } 394 395 // moduleStorage returns the module.Storage implementation used to store 396 // modules for commands. 397 func (m *Meta) moduleStorage(root string) getter.Storage { 398 return &uiModuleStorage{ 399 Storage: &getter.FolderStorage{ 400 StorageDir: filepath.Join(root, "modules"), 401 }, 402 Ui: m.Ui, 403 } 404 } 405 406 // process will process the meta-parameters out of the arguments. This 407 // will potentially modify the args in-place. It will return the resulting 408 // slice. 409 // 410 // vars says whether or not we support variables. 411 func (m *Meta) process(args []string, vars bool) []string { 412 // We do this so that we retain the ability to technically call 413 // process multiple times, even if we have no plans to do so 414 if m.oldUi != nil { 415 m.Ui = m.oldUi 416 } 417 418 // Set colorization 419 m.color = m.Color 420 for i, v := range args { 421 if v == "-no-color" { 422 m.color = false 423 m.Color = false 424 args = append(args[:i], args[i+1:]...) 425 break 426 } 427 } 428 429 // Set the UI 430 m.oldUi = m.Ui 431 m.Ui = &cli.ConcurrentUi{ 432 Ui: &ColorizeUi{ 433 Colorize: m.Colorize(), 434 ErrorColor: "[red]", 435 WarnColor: "[yellow]", 436 Ui: m.oldUi, 437 }, 438 } 439 440 // If we support vars and the default var file exists, add it to 441 // the args... 442 m.autoKey = "" 443 if vars { 444 if _, err := os.Stat(DefaultVarsFilename); err == nil { 445 m.autoKey = "var-file-default" 446 args = append(args, "", "") 447 copy(args[2:], args[0:]) 448 args[0] = "-" + m.autoKey 449 args[1] = DefaultVarsFilename 450 } 451 452 if _, err := os.Stat(DefaultVarsFilename + ".json"); err == nil { 453 m.autoKey = "var-file-default" 454 args = append(args, "", "") 455 copy(args[2:], args[0:]) 456 args[0] = "-" + m.autoKey 457 args[1] = DefaultVarsFilename + ".json" 458 } 459 } 460 461 return args 462 } 463 464 // uiHook returns the UiHook to use with the context. 465 func (m *Meta) uiHook() *UiHook { 466 return &UiHook{ 467 Colorize: m.Colorize(), 468 Ui: m.Ui, 469 } 470 } 471 472 const ( 473 // ModuleDepthDefault is the default value for 474 // module depth, which can be overridden by flag 475 // or env var 476 ModuleDepthDefault = -1 477 478 // ModuleDepthEnvVar is the name of the environment variable that can be used to set module depth. 479 ModuleDepthEnvVar = "TF_MODULE_DEPTH" 480 ) 481 482 func (m *Meta) addModuleDepthFlag(flags *flag.FlagSet, moduleDepth *int) { 483 flags.IntVar(moduleDepth, "module-depth", ModuleDepthDefault, "module-depth") 484 if envVar := os.Getenv(ModuleDepthEnvVar); envVar != "" { 485 if md, err := strconv.Atoi(envVar); err == nil { 486 *moduleDepth = md 487 } 488 } 489 } 490 491 // outputShadowError outputs the error from ctx.ShadowError. If the 492 // error is nil then nothing happens. If output is false then it isn't 493 // outputted to the user (you can define logic to guard against outputting). 494 func (m *Meta) outputShadowError(err error, output bool) bool { 495 // Do nothing if no error 496 if err == nil { 497 return false 498 } 499 500 // If not outputting, do nothing 501 if !output { 502 return false 503 } 504 505 // Write the shadow error output to a file 506 path := fmt.Sprintf("terraform-error-%d.log", time.Now().UTC().Unix()) 507 if err := ioutil.WriteFile(path, []byte(err.Error()), 0644); err != nil { 508 // If there is an error writing it, just let it go 509 log.Printf("[ERROR] Error writing shadow error: %s", err) 510 return false 511 } 512 513 // Output! 514 m.Ui.Output(m.Colorize().Color(fmt.Sprintf( 515 "[reset][bold][yellow]\nExperimental feature failure! Please report a bug.\n\n"+ 516 "This is not an error. Your Terraform operation completed successfully.\n"+ 517 "Your real infrastructure is unaffected by this message.\n\n"+ 518 "[reset][yellow]While running, Terraform sometimes tests experimental features in the\n"+ 519 "background. These features cannot affect real state and never touch\n"+ 520 "real infrastructure. If the features work properly, you see nothing.\n"+ 521 "If the features fail, this message appears.\n\n"+ 522 "You can report an issue at: https://github.com/hashicorp/terraform/issues\n\n"+ 523 "The failure was written to %q. Please\n"+ 524 "double check this file contains no sensitive information and report\n"+ 525 "it with your issue.\n\n"+ 526 "This is not an error. Your terraform operation completed successfully\n"+ 527 "and your real infrastructure is unaffected by this message.", 528 path, 529 ))) 530 531 return true 532 } 533 534 // contextOpts are the options used to load a context from a command. 535 type contextOpts struct { 536 // Path to the directory where the root module is. 537 // 538 // PathEmptyOk, when set, will allow paths that have no Terraform 539 // configurations. The result in that case will be an empty module. 540 Path string 541 PathEmptyOk bool 542 543 // StatePath is the path to the state file. If this is empty, then 544 // no state will be loaded. It is also okay for this to be a path to 545 // a file that doesn't exist; it is assumed that this means that there 546 // is simply no state. 547 StatePath string 548 549 // GetMode is the module.GetMode to use when loading the module tree. 550 GetMode module.GetMode 551 552 // Set to true when running a destroy plan/apply. 553 Destroy bool 554 555 // Number of concurrent operations allowed 556 Parallelism int 557 }