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