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