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