github.com/haklop/terraform@v0.3.6/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 if len(m.variables) > 0 { 110 return nil, false, fmt.Errorf( 111 "You can't set variables with the '-var' or '-var-file' flag\n" + 112 "when you're applying a plan file. The variables used when\n" + 113 "the plan was created will be used. If you wish to use different\n" + 114 "variable values, create a new plan file.") 115 } 116 117 return plan.Context(opts), true, nil 118 } 119 } 120 121 // Load the statePath if not given 122 if copts.StatePath != "" { 123 m.statePath = copts.StatePath 124 } 125 126 // Store the loaded state 127 state, err := m.loadState() 128 if err != nil { 129 return nil, false, err 130 } 131 m.state = state 132 133 // Load the root module 134 mod, err := module.NewTreeModule("", copts.Path) 135 if err != nil { 136 return nil, false, fmt.Errorf("Error loading config: %s", err) 137 } 138 139 dataDir := DefaultDataDirectory 140 if m.dataDir != "" { 141 dataDir = m.dataDir 142 } 143 err = mod.Load(m.moduleStorage(dataDir), copts.GetMode) 144 if err != nil { 145 return nil, false, fmt.Errorf("Error downloading modules: %s", err) 146 } 147 148 opts.Module = mod 149 opts.State = state 150 ctx := terraform.NewContext(opts) 151 return ctx, false, nil 152 } 153 154 // InputMode returns the type of input we should ask for in the form of 155 // terraform.InputMode which is passed directly to Context.Input. 156 func (m *Meta) InputMode() terraform.InputMode { 157 if test || !m.input { 158 return 0 159 } 160 161 var mode terraform.InputMode 162 mode |= terraform.InputModeProvider 163 if len(m.variables) == 0 && m.autoKey == "" { 164 mode |= terraform.InputModeVar 165 } 166 167 return mode 168 } 169 170 // UIInput returns a UIInput object to be used for asking for input. 171 func (m *Meta) UIInput() terraform.UIInput { 172 return &UIInput{ 173 Colorize: m.Colorize(), 174 } 175 } 176 177 // laodState is used to load the Terraform state. We give precedence 178 // to a remote state if enabled, and then check the normal state path. 179 func (m *Meta) loadState() (*terraform.State, error) { 180 // Check if we remote state is enabled 181 localCache, _, err := remote.ReadLocalState() 182 if err != nil { 183 return nil, fmt.Errorf("Error loading state: %s", err) 184 } 185 186 // Set the state if enabled 187 var state *terraform.State 188 if localCache != nil { 189 // Refresh the state 190 log.Printf("[INFO] Refreshing local state...") 191 changes, err := remote.RefreshState(localCache.Remote) 192 if err != nil { 193 return nil, fmt.Errorf("Failed to refresh state: %v", err) 194 } 195 switch changes { 196 case remote.StateChangeNoop: 197 case remote.StateChangeInit: 198 case remote.StateChangeLocalNewer: 199 case remote.StateChangeUpdateLocal: 200 // Reload the state since we've udpated 201 localCache, _, err = remote.ReadLocalState() 202 if err != nil { 203 return nil, fmt.Errorf("Error loading state: %s", err) 204 } 205 default: 206 return nil, fmt.Errorf("%s", changes) 207 } 208 209 state = localCache 210 m.useRemoteState = true 211 } 212 213 // Load up the state 214 if m.statePath != "" { 215 f, err := os.Open(m.statePath) 216 if err != nil && os.IsNotExist(err) { 217 // If the state file doesn't exist, it is okay, since it 218 // is probably a new infrastructure. 219 err = nil 220 } else if m.useRemoteState && err == nil { 221 err = fmt.Errorf("Remote state enabled, but state file '%s' also present.", m.statePath) 222 f.Close() 223 } else if err == nil { 224 state, err = terraform.ReadState(f) 225 f.Close() 226 } 227 if err != nil { 228 return nil, fmt.Errorf("Error loading state: %s", err) 229 } 230 } 231 return state, nil 232 } 233 234 // PersistState is used to write out the state, handling backup of 235 // the existing state file and respecting path configurations. 236 func (m *Meta) PersistState(s *terraform.State) error { 237 if m.useRemoteState { 238 return m.persistRemoteState(s) 239 } 240 return m.persistLocalState(s) 241 } 242 243 // persistRemoteState is used to handle persisting a state file 244 // when remote state management is enabled 245 func (m *Meta) persistRemoteState(s *terraform.State) error { 246 log.Printf("[INFO] Persisting state to local cache") 247 if err := remote.PersistState(s); err != nil { 248 return err 249 } 250 log.Printf("[INFO] Uploading state to remote store") 251 change, err := remote.PushState(s.Remote, false) 252 if err != nil { 253 return err 254 } 255 if !change.SuccessfulPush() { 256 return fmt.Errorf("Failed to upload state: %s", change) 257 } 258 return nil 259 } 260 261 // persistLocalState is used to handle persisting a state file 262 // when remote state management is disabled. 263 func (m *Meta) persistLocalState(s *terraform.State) error { 264 m.initStatePaths() 265 266 // Create a backup of the state before updating 267 if m.backupPath != "-" { 268 log.Printf("[INFO] Writing backup state to: %s", m.backupPath) 269 if err := remote.CopyFile(m.statePath, m.backupPath); err != nil { 270 return fmt.Errorf("Failed to backup state: %v", err) 271 } 272 } 273 274 // Open the new state file 275 fh, err := os.Create(m.stateOutPath) 276 if err != nil { 277 return fmt.Errorf("Failed to open state file: %v", err) 278 } 279 defer fh.Close() 280 281 // Write out the state 282 if err := terraform.WriteState(s, fh); err != nil { 283 return fmt.Errorf("Failed to encode the state: %v", err) 284 } 285 return nil 286 } 287 288 // Input returns true if we should ask for input for context. 289 func (m *Meta) Input() bool { 290 return !test && m.input && len(m.variables) == 0 291 } 292 293 // contextOpts returns the options to use to initialize a Terraform 294 // context with the settings from this Meta. 295 func (m *Meta) contextOpts() *terraform.ContextOpts { 296 var opts terraform.ContextOpts = *m.ContextOpts 297 opts.Hooks = make( 298 []terraform.Hook, 299 len(m.ContextOpts.Hooks)+len(m.extraHooks)+1) 300 opts.Hooks[0] = m.uiHook() 301 copy(opts.Hooks[1:], m.ContextOpts.Hooks) 302 copy(opts.Hooks[len(m.ContextOpts.Hooks)+1:], m.extraHooks) 303 304 vs := make(map[string]string) 305 for k, v := range opts.Variables { 306 vs[k] = v 307 } 308 for k, v := range m.autoVariables { 309 vs[k] = v 310 } 311 for k, v := range m.variables { 312 vs[k] = v 313 } 314 opts.Variables = vs 315 opts.UIInput = m.UIInput() 316 317 return &opts 318 } 319 320 // flags adds the meta flags to the given FlagSet. 321 func (m *Meta) flagSet(n string) *flag.FlagSet { 322 f := flag.NewFlagSet(n, flag.ContinueOnError) 323 f.BoolVar(&m.input, "input", true, "input") 324 f.Var((*FlagVar)(&m.variables), "var", "variables") 325 f.Var((*FlagVarFile)(&m.variables), "var-file", "variable file") 326 327 if m.autoKey != "" { 328 f.Var((*FlagVarFile)(&m.autoVariables), m.autoKey, "variable file") 329 } 330 331 // Create an io.Writer that writes to our Ui properly for errors. 332 // This is kind of a hack, but it does the job. Basically: create 333 // a pipe, use a scanner to break it into lines, and output each line 334 // to the UI. Do this forever. 335 errR, errW := io.Pipe() 336 errScanner := bufio.NewScanner(errR) 337 go func() { 338 for errScanner.Scan() { 339 m.Ui.Error(errScanner.Text()) 340 } 341 }() 342 f.SetOutput(errW) 343 344 return f 345 } 346 347 // moduleStorage returns the module.Storage implementation used to store 348 // modules for commands. 349 func (m *Meta) moduleStorage(root string) module.Storage { 350 return &uiModuleStorage{ 351 Storage: &module.FolderStorage{ 352 StorageDir: filepath.Join(root, "modules"), 353 }, 354 Ui: m.Ui, 355 } 356 } 357 358 // process will process the meta-parameters out of the arguments. This 359 // will potentially modify the args in-place. It will return the resulting 360 // slice. 361 // 362 // vars says whether or not we support variables. 363 func (m *Meta) process(args []string, vars bool) []string { 364 // We do this so that we retain the ability to technically call 365 // process multiple times, even if we have no plans to do so 366 if m.oldUi != nil { 367 m.Ui = m.oldUi 368 } 369 370 // Set colorization 371 m.color = m.Color 372 for i, v := range args { 373 if v == "-no-color" { 374 m.color = false 375 args = append(args[:i], args[i+1:]...) 376 break 377 } 378 } 379 380 // Set the UI 381 m.oldUi = m.Ui 382 m.Ui = &cli.ConcurrentUi{ 383 Ui: &ColorizeUi{ 384 Colorize: m.Colorize(), 385 ErrorColor: "[red]", 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 403 return args 404 } 405 406 // uiHook returns the UiHook to use with the context. 407 func (m *Meta) uiHook() *UiHook { 408 return &UiHook{ 409 Colorize: m.Colorize(), 410 Ui: m.Ui, 411 } 412 } 413 414 // contextOpts are the options used to load a context from a command. 415 type contextOpts struct { 416 // Path to the directory where the root module is. 417 Path string 418 419 // StatePath is the path to the state file. If this is empty, then 420 // no state will be loaded. It is also okay for this to be a path to 421 // a file that doesn't exist; it is assumed that this means that there 422 // is simply no state. 423 StatePath string 424 425 // GetMode is the module.GetMode to use when loading the module tree. 426 GetMode module.GetMode 427 }