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