github.com/peterbale/terraform@v0.9.0-beta2.0.20170315142748-5723acd55547/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 212 opts.Targets = m.targets 213 opts.UIInput = m.UIInput() 214 opts.Parallelism = m.parallelism 215 opts.Shadow = m.shadow 216 217 opts.Meta = &terraform.ContextMeta{ 218 Env: m.Env(), 219 } 220 221 return &opts 222 } 223 224 // flags adds the meta flags to the given FlagSet. 225 func (m *Meta) flagSet(n string) *flag.FlagSet { 226 f := flag.NewFlagSet(n, flag.ContinueOnError) 227 f.BoolVar(&m.input, "input", true, "input") 228 f.Var((*variables.Flag)(&m.variables), "var", "variables") 229 f.Var((*variables.FlagFile)(&m.variables), "var-file", "variable file") 230 f.Var((*FlagStringSlice)(&m.targets), "target", "resource to target") 231 232 if m.autoKey != "" { 233 f.Var((*variables.FlagFile)(&m.autoVariables), m.autoKey, "variable file") 234 } 235 236 // Advanced (don't need documentation, or unlikely to be set) 237 f.BoolVar(&m.shadow, "shadow", true, "shadow graph") 238 239 // Experimental features 240 experiment.Flag(f) 241 242 // Create an io.Writer that writes to our Ui properly for errors. 243 // This is kind of a hack, but it does the job. Basically: create 244 // a pipe, use a scanner to break it into lines, and output each line 245 // to the UI. Do this forever. 246 errR, errW := io.Pipe() 247 errScanner := bufio.NewScanner(errR) 248 go func() { 249 for errScanner.Scan() { 250 m.Ui.Error(errScanner.Text()) 251 } 252 }() 253 f.SetOutput(errW) 254 255 // Set the default Usage to empty 256 f.Usage = func() {} 257 258 return f 259 } 260 261 // moduleStorage returns the module.Storage implementation used to store 262 // modules for commands. 263 func (m *Meta) moduleStorage(root string) getter.Storage { 264 return &uiModuleStorage{ 265 Storage: &getter.FolderStorage{ 266 StorageDir: filepath.Join(root, "modules"), 267 }, 268 Ui: m.Ui, 269 } 270 } 271 272 // process will process the meta-parameters out of the arguments. This 273 // will potentially modify the args in-place. It will return the resulting 274 // slice. 275 // 276 // vars says whether or not we support variables. 277 func (m *Meta) process(args []string, vars bool) []string { 278 // We do this so that we retain the ability to technically call 279 // process multiple times, even if we have no plans to do so 280 if m.oldUi != nil { 281 m.Ui = m.oldUi 282 } 283 284 // Set colorization 285 m.color = m.Color 286 for i, v := range args { 287 if v == "-no-color" { 288 m.color = false 289 m.Color = false 290 args = append(args[:i], args[i+1:]...) 291 break 292 } 293 } 294 295 // Set the UI 296 m.oldUi = m.Ui 297 m.Ui = &cli.ConcurrentUi{ 298 Ui: &ColorizeUi{ 299 Colorize: m.Colorize(), 300 ErrorColor: "[red]", 301 WarnColor: "[yellow]", 302 Ui: m.oldUi, 303 }, 304 } 305 306 // If we support vars and the default var file exists, add it to 307 // the args... 308 m.autoKey = "" 309 if vars { 310 if _, err := os.Stat(DefaultVarsFilename); err == nil { 311 m.autoKey = "var-file-default" 312 args = append(args, "", "") 313 copy(args[2:], args[0:]) 314 args[0] = "-" + m.autoKey 315 args[1] = DefaultVarsFilename 316 } 317 318 if _, err := os.Stat(DefaultVarsFilename + ".json"); err == nil { 319 m.autoKey = "var-file-default" 320 args = append(args, "", "") 321 copy(args[2:], args[0:]) 322 args[0] = "-" + m.autoKey 323 args[1] = DefaultVarsFilename + ".json" 324 } 325 } 326 327 return args 328 } 329 330 // uiHook returns the UiHook to use with the context. 331 func (m *Meta) uiHook() *UiHook { 332 return &UiHook{ 333 Colorize: m.Colorize(), 334 Ui: m.Ui, 335 } 336 } 337 338 // confirm asks a yes/no confirmation. 339 func (m *Meta) confirm(opts *terraform.InputOpts) (bool, error) { 340 for { 341 v, err := m.UIInput().Input(opts) 342 if err != nil { 343 return false, fmt.Errorf( 344 "Error asking for confirmation: %s", err) 345 } 346 347 switch strings.ToLower(v) { 348 case "no": 349 return false, nil 350 case "yes": 351 return true, nil 352 } 353 } 354 } 355 356 const ( 357 // ModuleDepthDefault is the default value for 358 // module depth, which can be overridden by flag 359 // or env var 360 ModuleDepthDefault = -1 361 362 // ModuleDepthEnvVar is the name of the environment variable that can be used to set module depth. 363 ModuleDepthEnvVar = "TF_MODULE_DEPTH" 364 ) 365 366 func (m *Meta) addModuleDepthFlag(flags *flag.FlagSet, moduleDepth *int) { 367 flags.IntVar(moduleDepth, "module-depth", ModuleDepthDefault, "module-depth") 368 if envVar := os.Getenv(ModuleDepthEnvVar); envVar != "" { 369 if md, err := strconv.Atoi(envVar); err == nil { 370 *moduleDepth = md 371 } 372 } 373 } 374 375 // outputShadowError outputs the error from ctx.ShadowError. If the 376 // error is nil then nothing happens. If output is false then it isn't 377 // outputted to the user (you can define logic to guard against outputting). 378 func (m *Meta) outputShadowError(err error, output bool) bool { 379 // Do nothing if no error 380 if err == nil { 381 return false 382 } 383 384 // If not outputting, do nothing 385 if !output { 386 return false 387 } 388 389 // Write the shadow error output to a file 390 path := fmt.Sprintf("terraform-error-%d.log", time.Now().UTC().Unix()) 391 if err := ioutil.WriteFile(path, []byte(err.Error()), 0644); err != nil { 392 // If there is an error writing it, just let it go 393 log.Printf("[ERROR] Error writing shadow error: %s", err) 394 return false 395 } 396 397 // Output! 398 m.Ui.Output(m.Colorize().Color(fmt.Sprintf( 399 "[reset][bold][yellow]\nExperimental feature failure! Please report a bug.\n\n"+ 400 "This is not an error. Your Terraform operation completed successfully.\n"+ 401 "Your real infrastructure is unaffected by this message.\n\n"+ 402 "[reset][yellow]While running, Terraform sometimes tests experimental features in the\n"+ 403 "background. These features cannot affect real state and never touch\n"+ 404 "real infrastructure. If the features work properly, you see nothing.\n"+ 405 "If the features fail, this message appears.\n\n"+ 406 "You can report an issue at: https://github.com/hashicorp/terraform/issues\n\n"+ 407 "The failure was written to %q. Please\n"+ 408 "double check this file contains no sensitive information and report\n"+ 409 "it with your issue.\n\n"+ 410 "This is not an error. Your terraform operation completed successfully\n"+ 411 "and your real infrastructure is unaffected by this message.", 412 path, 413 ))) 414 415 return true 416 } 417 418 // Env returns the name of the currently configured environment, corresponding 419 // to the desired named state. 420 func (m *Meta) Env() string { 421 dataDir := m.dataDir 422 if m.dataDir == "" { 423 dataDir = DefaultDataDir 424 } 425 426 envData, err := ioutil.ReadFile(filepath.Join(dataDir, local.DefaultEnvFile)) 427 current := string(bytes.TrimSpace(envData)) 428 if current == "" { 429 current = backend.DefaultStateName 430 } 431 432 if err != nil && !os.IsNotExist(err) { 433 // always return the default if we can't get an environment name 434 log.Printf("[ERROR] failed to read current environment: %s", err) 435 } 436 437 return current 438 } 439 440 // SetEnv saves the named environment to the local filesystem. 441 func (m *Meta) SetEnv(name string) error { 442 dataDir := m.dataDir 443 if m.dataDir == "" { 444 dataDir = DefaultDataDir 445 } 446 447 err := os.MkdirAll(dataDir, 0755) 448 if err != nil { 449 return err 450 } 451 452 err = ioutil.WriteFile(filepath.Join(dataDir, local.DefaultEnvFile), []byte(name), 0644) 453 if err != nil { 454 return err 455 } 456 return nil 457 }