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