github.com/vtorhonen/terraform@v0.9.0-beta2.0.20170307220345-5d894e4ffda7/command/meta.go (about) 1 package command 2 3 import ( 4 "bufio" 5 "bytes" 6 "flag" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "log" 11 "os" 12 "path/filepath" 13 "strconv" 14 "strings" 15 "time" 16 17 "github.com/hashicorp/go-getter" 18 "github.com/hashicorp/terraform/backend" 19 "github.com/hashicorp/terraform/backend/local" 20 "github.com/hashicorp/terraform/helper/experiment" 21 "github.com/hashicorp/terraform/helper/variables" 22 "github.com/hashicorp/terraform/helper/wrappedstreams" 23 "github.com/hashicorp/terraform/terraform" 24 "github.com/mitchellh/cli" 25 "github.com/mitchellh/colorstring" 26 ) 27 28 // Meta are the meta-options that are available on all or most commands. 29 type Meta struct { 30 // The exported fields below should be set by anyone using a 31 // command with a Meta field. These are expected to be set externally 32 // (not from within the command itself). 33 34 Color bool // True if output should be colored 35 ContextOpts *terraform.ContextOpts // Opts copied to initialize 36 Ui cli.Ui // Ui for output 37 38 // ExtraHooks are extra hooks to add to the context. 39 ExtraHooks []terraform.Hook 40 41 //---------------------------------------------------------- 42 // Protected: commands can set these 43 //---------------------------------------------------------- 44 45 // Modify the data directory location. Defaults to DefaultDataDir 46 dataDir string 47 48 //---------------------------------------------------------- 49 // Private: do not set these 50 //---------------------------------------------------------- 51 52 // backendState is the currently active backend state 53 backendState *terraform.BackendState 54 55 // Variables for the context (private) 56 autoKey string 57 autoVariables map[string]interface{} 58 input bool 59 variables map[string]interface{} 60 61 // Targets for this context (private) 62 targets []string 63 64 // Internal fields 65 color bool 66 oldUi cli.Ui 67 68 // The fields below are expected to be set by the command via 69 // command line flags. See the Apply command for an example. 70 // 71 // statePath is the path to the state file. If this is empty, then 72 // no state will be loaded. It is also okay for this to be a path to 73 // a file that doesn't exist; it is assumed that this means that there 74 // is simply no state. 75 // 76 // stateOutPath is used to override the output path for the state. 77 // If not provided, the StatePath is used causing the old state to 78 // be overriden. 79 // 80 // backupPath is used to backup the state file before writing a modified 81 // version. It defaults to stateOutPath + DefaultBackupExtension 82 // 83 // parallelism is used to control the number of concurrent operations 84 // allowed when walking the graph 85 // 86 // shadow is used to enable/disable the shadow graph 87 // 88 // provider is to specify specific resource providers 89 // 90 // lockState is set to false to disable state locking 91 statePath string 92 stateOutPath string 93 backupPath string 94 parallelism int 95 shadow bool 96 provider string 97 stateLock bool 98 } 99 100 // initStatePaths is used to initialize the default values for 101 // statePath, stateOutPath, and backupPath 102 func (m *Meta) initStatePaths() { 103 if m.statePath == "" { 104 m.statePath = DefaultStateFilename 105 } 106 if m.stateOutPath == "" { 107 m.stateOutPath = m.statePath 108 } 109 if m.backupPath == "" { 110 m.backupPath = m.stateOutPath + DefaultBackupExtension 111 } 112 } 113 114 // StateOutPath returns the true output path for the state file 115 func (m *Meta) StateOutPath() string { 116 return m.stateOutPath 117 } 118 119 // Colorize returns the colorization structure for a command. 120 func (m *Meta) Colorize() *colorstring.Colorize { 121 return &colorstring.Colorize{ 122 Colors: colorstring.DefaultColors, 123 Disable: !m.color, 124 Reset: true, 125 } 126 } 127 128 // DataDir returns the directory where local data will be stored. 129 func (m *Meta) DataDir() string { 130 dataDir := DefaultDataDir 131 if m.dataDir != "" { 132 dataDir = m.dataDir 133 } 134 135 return dataDir 136 } 137 138 const ( 139 // InputModeEnvVar is the environment variable that, if set to "false" or 140 // "0", causes terraform commands to behave as if the `-input=false` flag was 141 // specified. 142 InputModeEnvVar = "TF_INPUT" 143 ) 144 145 // InputMode returns the type of input we should ask for in the form of 146 // terraform.InputMode which is passed directly to Context.Input. 147 func (m *Meta) InputMode() terraform.InputMode { 148 if test || !m.input { 149 return 0 150 } 151 152 if envVar := os.Getenv(InputModeEnvVar); envVar != "" { 153 if v, err := strconv.ParseBool(envVar); err == nil { 154 if !v { 155 return 0 156 } 157 } 158 } 159 160 var mode terraform.InputMode 161 mode |= terraform.InputModeProvider 162 mode |= terraform.InputModeVar 163 mode |= terraform.InputModeVarUnset 164 165 return mode 166 } 167 168 // UIInput returns a UIInput object to be used for asking for input. 169 func (m *Meta) UIInput() terraform.UIInput { 170 return &UIInput{ 171 Colorize: m.Colorize(), 172 } 173 } 174 175 // StdinPiped returns true if the input is piped. 176 func (m *Meta) StdinPiped() bool { 177 fi, err := wrappedstreams.Stdin().Stat() 178 if err != nil { 179 // If there is an error, let's just say its not piped 180 return false 181 } 182 183 return fi.Mode()&os.ModeNamedPipe != 0 184 } 185 186 // contextOpts returns the options to use to initialize a Terraform 187 // context with the settings from this Meta. 188 func (m *Meta) contextOpts() *terraform.ContextOpts { 189 var opts terraform.ContextOpts 190 if v := m.ContextOpts; v != nil { 191 opts = *v 192 } 193 194 opts.Hooks = []terraform.Hook{m.uiHook(), &terraform.DebugHook{}} 195 if m.ContextOpts != nil { 196 opts.Hooks = append(opts.Hooks, m.ContextOpts.Hooks...) 197 } 198 opts.Hooks = append(opts.Hooks, m.ExtraHooks...) 199 200 vs := make(map[string]interface{}) 201 for k, v := range opts.Variables { 202 vs[k] = v 203 } 204 for k, v := range m.autoVariables { 205 vs[k] = v 206 } 207 for k, v := range m.variables { 208 vs[k] = v 209 } 210 opts.Variables = vs 211 opts.Targets = m.targets 212 opts.UIInput = m.UIInput() 213 opts.Parallelism = m.parallelism 214 opts.Shadow = m.shadow 215 216 return &opts 217 } 218 219 // flags adds the meta flags to the given FlagSet. 220 func (m *Meta) flagSet(n string) *flag.FlagSet { 221 f := flag.NewFlagSet(n, flag.ContinueOnError) 222 f.BoolVar(&m.input, "input", true, "input") 223 f.Var((*variables.Flag)(&m.variables), "var", "variables") 224 f.Var((*variables.FlagFile)(&m.variables), "var-file", "variable file") 225 f.Var((*FlagStringSlice)(&m.targets), "target", "resource to target") 226 227 if m.autoKey != "" { 228 f.Var((*variables.FlagFile)(&m.autoVariables), m.autoKey, "variable file") 229 } 230 231 // Advanced (don't need documentation, or unlikely to be set) 232 f.BoolVar(&m.shadow, "shadow", true, "shadow graph") 233 234 // Experimental features 235 experiment.Flag(f) 236 237 // Create an io.Writer that writes to our Ui properly for errors. 238 // This is kind of a hack, but it does the job. Basically: create 239 // a pipe, use a scanner to break it into lines, and output each line 240 // to the UI. Do this forever. 241 errR, errW := io.Pipe() 242 errScanner := bufio.NewScanner(errR) 243 go func() { 244 for errScanner.Scan() { 245 m.Ui.Error(errScanner.Text()) 246 } 247 }() 248 f.SetOutput(errW) 249 250 // Set the default Usage to empty 251 f.Usage = func() {} 252 253 return f 254 } 255 256 // moduleStorage returns the module.Storage implementation used to store 257 // modules for commands. 258 func (m *Meta) moduleStorage(root string) getter.Storage { 259 return &uiModuleStorage{ 260 Storage: &getter.FolderStorage{ 261 StorageDir: filepath.Join(root, "modules"), 262 }, 263 Ui: m.Ui, 264 } 265 } 266 267 // process will process the meta-parameters out of the arguments. This 268 // will potentially modify the args in-place. It will return the resulting 269 // slice. 270 // 271 // vars says whether or not we support variables. 272 func (m *Meta) process(args []string, vars bool) []string { 273 // We do this so that we retain the ability to technically call 274 // process multiple times, even if we have no plans to do so 275 if m.oldUi != nil { 276 m.Ui = m.oldUi 277 } 278 279 // Set colorization 280 m.color = m.Color 281 for i, v := range args { 282 if v == "-no-color" { 283 m.color = false 284 m.Color = false 285 args = append(args[:i], args[i+1:]...) 286 break 287 } 288 } 289 290 // Set the UI 291 m.oldUi = m.Ui 292 m.Ui = &cli.ConcurrentUi{ 293 Ui: &ColorizeUi{ 294 Colorize: m.Colorize(), 295 ErrorColor: "[red]", 296 WarnColor: "[yellow]", 297 Ui: m.oldUi, 298 }, 299 } 300 301 // If we support vars and the default var file exists, add it to 302 // the args... 303 m.autoKey = "" 304 if vars { 305 if _, err := os.Stat(DefaultVarsFilename); err == nil { 306 m.autoKey = "var-file-default" 307 args = append(args, "", "") 308 copy(args[2:], args[0:]) 309 args[0] = "-" + m.autoKey 310 args[1] = DefaultVarsFilename 311 } 312 313 if _, err := os.Stat(DefaultVarsFilename + ".json"); err == nil { 314 m.autoKey = "var-file-default" 315 args = append(args, "", "") 316 copy(args[2:], args[0:]) 317 args[0] = "-" + m.autoKey 318 args[1] = DefaultVarsFilename + ".json" 319 } 320 } 321 322 return args 323 } 324 325 // uiHook returns the UiHook to use with the context. 326 func (m *Meta) uiHook() *UiHook { 327 return &UiHook{ 328 Colorize: m.Colorize(), 329 Ui: m.Ui, 330 } 331 } 332 333 // confirm asks a yes/no confirmation. 334 func (m *Meta) confirm(opts *terraform.InputOpts) (bool, error) { 335 for { 336 v, err := m.UIInput().Input(opts) 337 if err != nil { 338 return false, fmt.Errorf( 339 "Error asking for confirmation: %s", err) 340 } 341 342 switch strings.ToLower(v) { 343 case "no": 344 return false, nil 345 case "yes": 346 return true, nil 347 } 348 } 349 } 350 351 const ( 352 // ModuleDepthDefault is the default value for 353 // module depth, which can be overridden by flag 354 // or env var 355 ModuleDepthDefault = -1 356 357 // ModuleDepthEnvVar is the name of the environment variable that can be used to set module depth. 358 ModuleDepthEnvVar = "TF_MODULE_DEPTH" 359 ) 360 361 func (m *Meta) addModuleDepthFlag(flags *flag.FlagSet, moduleDepth *int) { 362 flags.IntVar(moduleDepth, "module-depth", ModuleDepthDefault, "module-depth") 363 if envVar := os.Getenv(ModuleDepthEnvVar); envVar != "" { 364 if md, err := strconv.Atoi(envVar); err == nil { 365 *moduleDepth = md 366 } 367 } 368 } 369 370 // outputShadowError outputs the error from ctx.ShadowError. If the 371 // error is nil then nothing happens. If output is false then it isn't 372 // outputted to the user (you can define logic to guard against outputting). 373 func (m *Meta) outputShadowError(err error, output bool) bool { 374 // Do nothing if no error 375 if err == nil { 376 return false 377 } 378 379 // If not outputting, do nothing 380 if !output { 381 return false 382 } 383 384 // Write the shadow error output to a file 385 path := fmt.Sprintf("terraform-error-%d.log", time.Now().UTC().Unix()) 386 if err := ioutil.WriteFile(path, []byte(err.Error()), 0644); err != nil { 387 // If there is an error writing it, just let it go 388 log.Printf("[ERROR] Error writing shadow error: %s", err) 389 return false 390 } 391 392 // Output! 393 m.Ui.Output(m.Colorize().Color(fmt.Sprintf( 394 "[reset][bold][yellow]\nExperimental feature failure! Please report a bug.\n\n"+ 395 "This is not an error. Your Terraform operation completed successfully.\n"+ 396 "Your real infrastructure is unaffected by this message.\n\n"+ 397 "[reset][yellow]While running, Terraform sometimes tests experimental features in the\n"+ 398 "background. These features cannot affect real state and never touch\n"+ 399 "real infrastructure. If the features work properly, you see nothing.\n"+ 400 "If the features fail, this message appears.\n\n"+ 401 "You can report an issue at: https://github.com/hashicorp/terraform/issues\n\n"+ 402 "The failure was written to %q. Please\n"+ 403 "double check this file contains no sensitive information and report\n"+ 404 "it with your issue.\n\n"+ 405 "This is not an error. Your terraform operation completed successfully\n"+ 406 "and your real infrastructure is unaffected by this message.", 407 path, 408 ))) 409 410 return true 411 } 412 413 // Env returns the name of the currently configured environment, corresponding 414 // to the desired named state. 415 func (m *Meta) Env() string { 416 dataDir := m.dataDir 417 if m.dataDir == "" { 418 dataDir = DefaultDataDir 419 } 420 421 envData, err := ioutil.ReadFile(filepath.Join(dataDir, local.DefaultEnvFile)) 422 current := string(bytes.TrimSpace(envData)) 423 if current == "" { 424 current = backend.DefaultStateName 425 } 426 427 if err != nil && !os.IsNotExist(err) { 428 // always return the default if we can't get an environment name 429 log.Printf("[ERROR] failed to read current environment: %s", err) 430 } 431 432 return current 433 } 434 435 // SetEnv saves the named environment to the local filesystem. 436 func (m *Meta) SetEnv(name string) error { 437 dataDir := m.dataDir 438 if m.dataDir == "" { 439 dataDir = DefaultDataDir 440 } 441 442 err := os.MkdirAll(dataDir, 0755) 443 if err != nil { 444 return err 445 } 446 447 err = ioutil.WriteFile(filepath.Join(dataDir, local.DefaultEnvFile), []byte(name), 0644) 448 if err != nil { 449 return err 450 } 451 return nil 452 }