github.com/chalford/terraform@v0.3.7-0.20150113080010-a78c69a8c81f/command/meta.go (about) 1 package command 2 3 import ( 4 "bufio" 5 "flag" 6 "fmt" 7 "io" 8 "log" 9 "os" 10 "path/filepath" 11 12 "github.com/hashicorp/terraform/config/module" 13 "github.com/hashicorp/terraform/remote" 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 *terraform.State 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 // useRemoteState is enabled if we are using remote state storage 45 // This is set when the context is loaded if we read from a remote 46 // enabled state file. 47 useRemoteState bool 48 49 // statePath is the path to the state file. If this is empty, then 50 // no state will be loaded. It is also okay for this to be a path to 51 // a file that doesn't exist; it is assumed that this means that there 52 // is simply no state. 53 statePath string 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 stateOutPath string 59 60 // backupPath is used to backup the state file before writing a modified 61 // version. It defaults to stateOutPath + DefaultBackupExtention 62 backupPath string 63 } 64 65 // initStatePaths is used to initialize the default values for 66 // statePath, stateOutPath, and backupPath 67 func (m *Meta) initStatePaths() { 68 if m.statePath == "" { 69 m.statePath = DefaultStateFilename 70 } 71 if m.stateOutPath == "" { 72 m.stateOutPath = m.statePath 73 } 74 if m.backupPath == "" { 75 m.backupPath = m.stateOutPath + DefaultBackupExtention 76 } 77 } 78 79 // StateOutPath returns the true output path for the state file 80 func (m *Meta) StateOutPath() string { 81 m.initStatePaths() 82 if m.useRemoteState { 83 path, _ := remote.HiddenStatePath() 84 return path 85 } 86 return m.stateOutPath 87 } 88 89 // Colorize returns the colorization structure for a command. 90 func (m *Meta) Colorize() *colorstring.Colorize { 91 return &colorstring.Colorize{ 92 Colors: colorstring.DefaultColors, 93 Disable: !m.color, 94 Reset: true, 95 } 96 } 97 98 // Context returns a Terraform Context taking into account the context 99 // options used to initialize this meta configuration. 100 func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) { 101 opts := m.contextOpts() 102 103 // First try to just read the plan directly from the path given. 104 f, err := os.Open(copts.Path) 105 if err == nil { 106 plan, err := terraform.ReadPlan(f) 107 f.Close() 108 if err == nil { 109 // Check if remote state is enabled, but do not refresh. 110 // Since a plan is supposed to lock-in the changes, we do not 111 // attempt a state refresh. 112 if plan != nil && plan.State != nil && plan.State.Remote != nil && plan.State.Remote.Type != "" { 113 log.Printf("[INFO] Enabling remote state from plan") 114 m.useRemoteState = true 115 } 116 117 if len(m.variables) > 0 { 118 return nil, false, fmt.Errorf( 119 "You can't set variables with the '-var' or '-var-file' flag\n" + 120 "when you're applying a plan file. The variables used when\n" + 121 "the plan was created will be used. If you wish to use different\n" + 122 "variable values, create a new plan file.") 123 } 124 125 return plan.Context(opts), true, nil 126 } 127 } 128 129 // Load the statePath if not given 130 if copts.StatePath != "" { 131 m.statePath = copts.StatePath 132 } 133 134 // Store the loaded state 135 state, err := m.loadState() 136 if err != nil { 137 return nil, false, err 138 } 139 m.state = state 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 dataDir := DefaultDataDirectory 148 if m.dataDir != "" { 149 dataDir = m.dataDir 150 } 151 err = mod.Load(m.moduleStorage(dataDir), copts.GetMode) 152 if err != nil { 153 return nil, false, fmt.Errorf("Error downloading modules: %s", err) 154 } 155 156 opts.Module = mod 157 opts.State = state 158 ctx := terraform.NewContext(opts) 159 return ctx, false, nil 160 } 161 162 // InputMode returns the type of input we should ask for in the form of 163 // terraform.InputMode which is passed directly to Context.Input. 164 func (m *Meta) InputMode() terraform.InputMode { 165 if test || !m.input { 166 return 0 167 } 168 169 var mode terraform.InputMode 170 mode |= terraform.InputModeProvider 171 if len(m.variables) == 0 && m.autoKey == "" { 172 mode |= terraform.InputModeVar 173 } 174 175 return mode 176 } 177 178 // UIInput returns a UIInput object to be used for asking for input. 179 func (m *Meta) UIInput() terraform.UIInput { 180 return &UIInput{ 181 Colorize: m.Colorize(), 182 } 183 } 184 185 // laodState is used to load the Terraform state. We give precedence 186 // to a remote state if enabled, and then check the normal state path. 187 func (m *Meta) loadState() (*terraform.State, error) { 188 // Check if we remote state is enabled 189 localCache, _, err := remote.ReadLocalState() 190 if err != nil { 191 return nil, fmt.Errorf("Error loading state: %s", err) 192 } 193 194 // Set the state if enabled 195 var state *terraform.State 196 if localCache != nil { 197 // Refresh the state 198 log.Printf("[INFO] Refreshing local state...") 199 changes, err := remote.RefreshState(localCache.Remote) 200 if err != nil { 201 return nil, fmt.Errorf("Failed to refresh state: %v", err) 202 } 203 switch changes { 204 case remote.StateChangeNoop: 205 case remote.StateChangeInit: 206 case remote.StateChangeLocalNewer: 207 case remote.StateChangeUpdateLocal: 208 // Reload the state since we've udpated 209 localCache, _, err = remote.ReadLocalState() 210 if err != nil { 211 return nil, fmt.Errorf("Error loading state: %s", err) 212 } 213 default: 214 return nil, fmt.Errorf("%s", changes) 215 } 216 217 state = localCache 218 m.useRemoteState = true 219 } 220 221 // Load up the state 222 if m.statePath != "" { 223 f, err := os.Open(m.statePath) 224 if err != nil && os.IsNotExist(err) { 225 // If the state file doesn't exist, it is okay, since it 226 // is probably a new infrastructure. 227 err = nil 228 } else if m.useRemoteState && err == nil { 229 err = fmt.Errorf("Remote state enabled, but state file '%s' also present.", m.statePath) 230 f.Close() 231 } else if err == nil { 232 state, err = terraform.ReadState(f) 233 f.Close() 234 } 235 if err != nil { 236 return nil, fmt.Errorf("Error loading state: %s", err) 237 } 238 } 239 return state, nil 240 } 241 242 // PersistState is used to write out the state, handling backup of 243 // the existing state file and respecting path configurations. 244 func (m *Meta) PersistState(s *terraform.State) error { 245 if m.useRemoteState { 246 return m.persistRemoteState(s) 247 } 248 return m.persistLocalState(s) 249 } 250 251 // persistRemoteState is used to handle persisting a state file 252 // when remote state management is enabled 253 func (m *Meta) persistRemoteState(s *terraform.State) error { 254 log.Printf("[INFO] Persisting state to local cache") 255 if err := remote.PersistState(s); err != nil { 256 return err 257 } 258 log.Printf("[INFO] Uploading state to remote store") 259 change, err := remote.PushState(s.Remote, false) 260 if err != nil { 261 return err 262 } 263 if !change.SuccessfulPush() { 264 return fmt.Errorf("Failed to upload state: %s", change) 265 } 266 return nil 267 } 268 269 // persistLocalState is used to handle persisting a state file 270 // when remote state management is disabled. 271 func (m *Meta) persistLocalState(s *terraform.State) error { 272 m.initStatePaths() 273 274 // Create a backup of the state before updating 275 if m.backupPath != "-" { 276 log.Printf("[INFO] Writing backup state to: %s", m.backupPath) 277 if err := remote.CopyFile(m.statePath, m.backupPath); err != nil { 278 return fmt.Errorf("Failed to backup state: %v", err) 279 } 280 } 281 282 // Open the new state file 283 fh, err := os.Create(m.stateOutPath) 284 if err != nil { 285 return fmt.Errorf("Failed to open state file: %v", err) 286 } 287 defer fh.Close() 288 289 // Write out the state 290 if err := terraform.WriteState(s, fh); err != nil { 291 return fmt.Errorf("Failed to encode the state: %v", err) 292 } 293 return nil 294 } 295 296 // Input returns true if we should ask for input for context. 297 func (m *Meta) Input() bool { 298 return !test && m.input && len(m.variables) == 0 299 } 300 301 // contextOpts returns the options to use to initialize a Terraform 302 // context with the settings from this Meta. 303 func (m *Meta) contextOpts() *terraform.ContextOpts { 304 var opts terraform.ContextOpts = *m.ContextOpts 305 opts.Hooks = make( 306 []terraform.Hook, 307 len(m.ContextOpts.Hooks)+len(m.extraHooks)+1) 308 opts.Hooks[0] = m.uiHook() 309 copy(opts.Hooks[1:], m.ContextOpts.Hooks) 310 copy(opts.Hooks[len(m.ContextOpts.Hooks)+1:], m.extraHooks) 311 312 vs := make(map[string]string) 313 for k, v := range opts.Variables { 314 vs[k] = v 315 } 316 for k, v := range m.autoVariables { 317 vs[k] = v 318 } 319 for k, v := range m.variables { 320 vs[k] = v 321 } 322 opts.Variables = vs 323 opts.UIInput = m.UIInput() 324 325 return &opts 326 } 327 328 // flags adds the meta flags to the given FlagSet. 329 func (m *Meta) flagSet(n string) *flag.FlagSet { 330 f := flag.NewFlagSet(n, flag.ContinueOnError) 331 f.BoolVar(&m.input, "input", true, "input") 332 f.Var((*FlagVar)(&m.variables), "var", "variables") 333 f.Var((*FlagVarFile)(&m.variables), "var-file", "variable file") 334 335 if m.autoKey != "" { 336 f.Var((*FlagVarFile)(&m.autoVariables), m.autoKey, "variable file") 337 } 338 339 // Create an io.Writer that writes to our Ui properly for errors. 340 // This is kind of a hack, but it does the job. Basically: create 341 // a pipe, use a scanner to break it into lines, and output each line 342 // to the UI. Do this forever. 343 errR, errW := io.Pipe() 344 errScanner := bufio.NewScanner(errR) 345 go func() { 346 for errScanner.Scan() { 347 m.Ui.Error(errScanner.Text()) 348 } 349 }() 350 f.SetOutput(errW) 351 352 return f 353 } 354 355 // moduleStorage returns the module.Storage implementation used to store 356 // modules for commands. 357 func (m *Meta) moduleStorage(root string) module.Storage { 358 return &uiModuleStorage{ 359 Storage: &module.FolderStorage{ 360 StorageDir: filepath.Join(root, "modules"), 361 }, 362 Ui: m.Ui, 363 } 364 } 365 366 // process will process the meta-parameters out of the arguments. This 367 // will potentially modify the args in-place. It will return the resulting 368 // slice. 369 // 370 // vars says whether or not we support variables. 371 func (m *Meta) process(args []string, vars bool) []string { 372 // We do this so that we retain the ability to technically call 373 // process multiple times, even if we have no plans to do so 374 if m.oldUi != nil { 375 m.Ui = m.oldUi 376 } 377 378 // Set colorization 379 m.color = m.Color 380 for i, v := range args { 381 if v == "-no-color" { 382 m.color = false 383 args = append(args[:i], args[i+1:]...) 384 break 385 } 386 } 387 388 // Set the UI 389 m.oldUi = m.Ui 390 m.Ui = &cli.ConcurrentUi{ 391 Ui: &ColorizeUi{ 392 Colorize: m.Colorize(), 393 ErrorColor: "[red]", 394 Ui: m.oldUi, 395 }, 396 } 397 398 // If we support vars and the default var file exists, add it to 399 // the args... 400 m.autoKey = "" 401 if vars { 402 if _, err := os.Stat(DefaultVarsFilename); 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 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 // contextOpts are the options used to load a context from a command. 423 type contextOpts struct { 424 // Path to the directory where the root module is. 425 Path string 426 427 // StatePath is the path to the state file. If this is empty, then 428 // no state will be loaded. It is also okay for this to be a path to 429 // a file that doesn't exist; it is assumed that this means that there 430 // is simply no state. 431 StatePath string 432 433 // GetMode is the module.GetMode to use when loading the module tree. 434 GetMode module.GetMode 435 }