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