github.com/grange74/terraform@v0.7.0-rc3.0.20160722171430-8c8803864753/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]string 40 input bool 41 variables map[string]string 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 opts.Module = mod 169 opts.Parallelism = copts.Parallelism 170 opts.State = state.State() 171 ctx, err := terraform.NewContext(opts) 172 return ctx, false, err 173 } 174 175 // DataDir returns the directory where local data will be stored. 176 func (m *Meta) DataDir() string { 177 dataDir := DefaultDataDir 178 if m.dataDir != "" { 179 dataDir = m.dataDir 180 } 181 182 return dataDir 183 } 184 185 const ( 186 // InputModeEnvVar is the environment variable that, if set to "false" or 187 // "0", causes terraform commands to behave as if the `-input=false` flag was 188 // specified. 189 InputModeEnvVar = "TF_INPUT" 190 ) 191 192 // InputMode returns the type of input we should ask for in the form of 193 // terraform.InputMode which is passed directly to Context.Input. 194 func (m *Meta) InputMode() terraform.InputMode { 195 if test || !m.input { 196 return 0 197 } 198 199 if envVar := os.Getenv(InputModeEnvVar); envVar != "" { 200 if v, err := strconv.ParseBool(envVar); err == nil { 201 if !v { 202 return 0 203 } 204 } 205 } 206 207 var mode terraform.InputMode 208 mode |= terraform.InputModeProvider 209 if len(m.variables) == 0 { 210 mode |= terraform.InputModeVar 211 mode |= terraform.InputModeVarUnset 212 } 213 214 return mode 215 } 216 217 // State returns the state for this meta. 218 func (m *Meta) State() (state.State, error) { 219 if m.state != nil { 220 return m.state, nil 221 } 222 223 result, err := State(m.StateOpts()) 224 if err != nil { 225 return nil, err 226 } 227 228 m.state = result.State 229 m.stateOutPath = result.StatePath 230 m.stateResult = result 231 return m.state, nil 232 } 233 234 // StateRaw is used to setup the state manually. 235 func (m *Meta) StateRaw(opts *StateOpts) (*StateResult, error) { 236 result, err := State(opts) 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 result, nil 245 } 246 247 // StateOpts returns the default state options 248 func (m *Meta) StateOpts() *StateOpts { 249 localPath := m.statePath 250 if localPath == "" { 251 localPath = DefaultStateFilename 252 } 253 remotePath := filepath.Join(m.DataDir(), DefaultStateFilename) 254 255 return &StateOpts{ 256 LocalPath: localPath, 257 LocalPathOut: m.stateOutPath, 258 RemotePath: remotePath, 259 RemoteRefresh: true, 260 BackupPath: m.backupPath, 261 } 262 } 263 264 // UIInput returns a UIInput object to be used for asking for input. 265 func (m *Meta) UIInput() terraform.UIInput { 266 return &UIInput{ 267 Colorize: m.Colorize(), 268 } 269 } 270 271 // PersistState is used to write out the state, handling backup of 272 // the existing state file and respecting path configurations. 273 func (m *Meta) PersistState(s *terraform.State) error { 274 if err := m.state.WriteState(s); err != nil { 275 return err 276 } 277 278 return m.state.PersistState() 279 } 280 281 // Input returns true if we should ask for input for context. 282 func (m *Meta) Input() bool { 283 return !test && m.input && len(m.variables) == 0 284 } 285 286 // contextOpts returns the options to use to initialize a Terraform 287 // context with the settings from this Meta. 288 func (m *Meta) contextOpts() *terraform.ContextOpts { 289 var opts terraform.ContextOpts = *m.ContextOpts 290 opts.Hooks = make( 291 []terraform.Hook, 292 len(m.ContextOpts.Hooks)+len(m.extraHooks)+1) 293 opts.Hooks[0] = m.uiHook() 294 copy(opts.Hooks[1:], m.ContextOpts.Hooks) 295 copy(opts.Hooks[len(m.ContextOpts.Hooks)+1:], m.extraHooks) 296 297 vs := make(map[string]interface{}) 298 for k, v := range opts.Variables { 299 vs[k] = v 300 } 301 for k, v := range m.autoVariables { 302 vs[k] = v 303 } 304 for k, v := range m.variables { 305 vs[k] = v 306 } 307 opts.Variables = vs 308 opts.Targets = m.targets 309 opts.UIInput = m.UIInput() 310 311 return &opts 312 } 313 314 // flags adds the meta flags to the given FlagSet. 315 func (m *Meta) flagSet(n string) *flag.FlagSet { 316 f := flag.NewFlagSet(n, flag.ContinueOnError) 317 f.BoolVar(&m.input, "input", true, "input") 318 f.Var((*FlagKV)(&m.variables), "var", "variables") 319 f.Var((*FlagKVFile)(&m.variables), "var-file", "variable file") 320 f.Var((*FlagStringSlice)(&m.targets), "target", "resource to target") 321 322 if m.autoKey != "" { 323 f.Var((*FlagKVFile)(&m.autoVariables), m.autoKey, "variable file") 324 } 325 326 // Create an io.Writer that writes to our Ui properly for errors. 327 // This is kind of a hack, but it does the job. Basically: create 328 // a pipe, use a scanner to break it into lines, and output each line 329 // to the UI. Do this forever. 330 errR, errW := io.Pipe() 331 errScanner := bufio.NewScanner(errR) 332 go func() { 333 for errScanner.Scan() { 334 m.Ui.Error(errScanner.Text()) 335 } 336 }() 337 f.SetOutput(errW) 338 339 // Set the default Usage to empty 340 f.Usage = func() {} 341 342 return f 343 } 344 345 // moduleStorage returns the module.Storage implementation used to store 346 // modules for commands. 347 func (m *Meta) moduleStorage(root string) getter.Storage { 348 return &uiModuleStorage{ 349 Storage: &getter.FolderStorage{ 350 StorageDir: filepath.Join(root, "modules"), 351 }, 352 Ui: m.Ui, 353 } 354 } 355 356 // process will process the meta-parameters out of the arguments. This 357 // will potentially modify the args in-place. It will return the resulting 358 // slice. 359 // 360 // vars says whether or not we support variables. 361 func (m *Meta) process(args []string, vars bool) []string { 362 // We do this so that we retain the ability to technically call 363 // process multiple times, even if we have no plans to do so 364 if m.oldUi != nil { 365 m.Ui = m.oldUi 366 } 367 368 // Set colorization 369 m.color = m.Color 370 for i, v := range args { 371 if v == "-no-color" { 372 m.color = false 373 m.Color = false 374 args = append(args[:i], args[i+1:]...) 375 break 376 } 377 } 378 379 // Set the UI 380 m.oldUi = m.Ui 381 m.Ui = &cli.ConcurrentUi{ 382 Ui: &ColorizeUi{ 383 Colorize: m.Colorize(), 384 ErrorColor: "[red]", 385 WarnColor: "[yellow]", 386 Ui: m.oldUi, 387 }, 388 } 389 390 // If we support vars and the default var file exists, add it to 391 // the args... 392 m.autoKey = "" 393 if vars { 394 if _, err := os.Stat(DefaultVarsFilename); err == nil { 395 m.autoKey = "var-file-default" 396 args = append(args, "", "") 397 copy(args[2:], args[0:]) 398 args[0] = "-" + m.autoKey 399 args[1] = DefaultVarsFilename 400 } 401 402 if _, err := os.Stat(DefaultVarsFilename + ".json"); err == nil { 403 m.autoKey = "var-file-default" 404 args = append(args, "", "") 405 copy(args[2:], args[0:]) 406 args[0] = "-" + m.autoKey 407 args[1] = DefaultVarsFilename + ".json" 408 } 409 } 410 411 return args 412 } 413 414 // uiHook returns the UiHook to use with the context. 415 func (m *Meta) uiHook() *UiHook { 416 return &UiHook{ 417 Colorize: m.Colorize(), 418 Ui: m.Ui, 419 } 420 } 421 422 const ( 423 // ModuleDepthDefault is the default value for 424 // module depth, which can be overridden by flag 425 // or env var 426 ModuleDepthDefault = -1 427 428 // ModuleDepthEnvVar is the name of the environment variable that can be used to set module depth. 429 ModuleDepthEnvVar = "TF_MODULE_DEPTH" 430 ) 431 432 func (m *Meta) addModuleDepthFlag(flags *flag.FlagSet, moduleDepth *int) { 433 flags.IntVar(moduleDepth, "module-depth", ModuleDepthDefault, "module-depth") 434 if envVar := os.Getenv(ModuleDepthEnvVar); envVar != "" { 435 if md, err := strconv.Atoi(envVar); err == nil { 436 *moduleDepth = md 437 } 438 } 439 } 440 441 // contextOpts are the options used to load a context from a command. 442 type contextOpts struct { 443 // Path to the directory where the root module is. 444 Path string 445 446 // StatePath is the path to the state file. If this is empty, then 447 // no state will be loaded. It is also okay for this to be a path to 448 // a file that doesn't exist; it is assumed that this means that there 449 // is simply no state. 450 StatePath string 451 452 // GetMode is the module.GetMode to use when loading the module tree. 453 GetMode module.GetMode 454 455 // Set to true when running a destroy plan/apply. 456 Destroy bool 457 458 // Number of concurrent operations allowed 459 Parallelism int 460 }