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