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