github.com/cbroglie/terraform@v0.7.0-rc3.0.20170410193827-735dfc416d46/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 // stateLockTimeout is the optional duration to retry a state locks locks 94 // when it is already locked by another process. 95 // 96 // forceInitCopy suppresses confirmation for copying state data during 97 // init. 98 statePath string 99 stateOutPath string 100 backupPath string 101 parallelism int 102 shadow bool 103 provider string 104 stateLock bool 105 stateLockTimeout time.Duration 106 forceInitCopy bool 107 } 108 109 // initStatePaths is used to initialize the default values for 110 // statePath, stateOutPath, and backupPath 111 func (m *Meta) initStatePaths() { 112 if m.statePath == "" { 113 m.statePath = DefaultStateFilename 114 } 115 if m.stateOutPath == "" { 116 m.stateOutPath = m.statePath 117 } 118 if m.backupPath == "" { 119 m.backupPath = m.stateOutPath + DefaultBackupExtension 120 } 121 } 122 123 // StateOutPath returns the true output path for the state file 124 func (m *Meta) StateOutPath() string { 125 return m.stateOutPath 126 } 127 128 // Colorize returns the colorization structure for a command. 129 func (m *Meta) Colorize() *colorstring.Colorize { 130 return &colorstring.Colorize{ 131 Colors: colorstring.DefaultColors, 132 Disable: !m.color, 133 Reset: true, 134 } 135 } 136 137 // DataDir returns the directory where local data will be stored. 138 func (m *Meta) DataDir() string { 139 dataDir := DefaultDataDir 140 if m.dataDir != "" { 141 dataDir = m.dataDir 142 } 143 144 return dataDir 145 } 146 147 const ( 148 // InputModeEnvVar is the environment variable that, if set to "false" or 149 // "0", causes terraform commands to behave as if the `-input=false` flag was 150 // specified. 151 InputModeEnvVar = "TF_INPUT" 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 if envVar := os.Getenv(InputModeEnvVar); envVar != "" { 162 if v, err := strconv.ParseBool(envVar); err == nil { 163 if !v { 164 return 0 165 } 166 } 167 } 168 169 var mode terraform.InputMode 170 mode |= terraform.InputModeProvider 171 mode |= terraform.InputModeVar 172 mode |= terraform.InputModeVarUnset 173 174 return mode 175 } 176 177 // UIInput returns a UIInput object to be used for asking for input. 178 func (m *Meta) UIInput() terraform.UIInput { 179 return &UIInput{ 180 Colorize: m.Colorize(), 181 } 182 } 183 184 // StdinPiped returns true if the input is piped. 185 func (m *Meta) StdinPiped() bool { 186 fi, err := wrappedstreams.Stdin().Stat() 187 if err != nil { 188 // If there is an error, let's just say its not piped 189 return false 190 } 191 192 return fi.Mode()&os.ModeNamedPipe != 0 193 } 194 195 // contextOpts returns the options to use to initialize a Terraform 196 // context with the settings from this Meta. 197 func (m *Meta) contextOpts() *terraform.ContextOpts { 198 var opts terraform.ContextOpts 199 if v := m.ContextOpts; v != nil { 200 opts = *v 201 } 202 203 opts.Hooks = []terraform.Hook{m.uiHook(), &terraform.DebugHook{}} 204 if m.ContextOpts != nil { 205 opts.Hooks = append(opts.Hooks, m.ContextOpts.Hooks...) 206 } 207 opts.Hooks = append(opts.Hooks, m.ExtraHooks...) 208 209 vs := make(map[string]interface{}) 210 for k, v := range opts.Variables { 211 vs[k] = v 212 } 213 for k, v := range m.autoVariables { 214 vs[k] = v 215 } 216 for k, v := range m.variables { 217 vs[k] = v 218 } 219 opts.Variables = vs 220 221 opts.Targets = m.targets 222 opts.UIInput = m.UIInput() 223 opts.Parallelism = m.parallelism 224 opts.Shadow = m.shadow 225 226 opts.Meta = &terraform.ContextMeta{ 227 Env: m.Env(), 228 } 229 230 return &opts 231 } 232 233 // flags adds the meta flags to the given FlagSet. 234 func (m *Meta) flagSet(n string) *flag.FlagSet { 235 f := flag.NewFlagSet(n, flag.ContinueOnError) 236 f.BoolVar(&m.input, "input", true, "input") 237 f.Var((*variables.Flag)(&m.variables), "var", "variables") 238 f.Var((*variables.FlagFile)(&m.variables), "var-file", "variable file") 239 f.Var((*FlagStringSlice)(&m.targets), "target", "resource to target") 240 241 if m.autoKey != "" { 242 f.Var((*variables.FlagFile)(&m.autoVariables), m.autoKey, "variable file") 243 } 244 245 // Advanced (don't need documentation, or unlikely to be set) 246 f.BoolVar(&m.shadow, "shadow", true, "shadow graph") 247 248 // Experimental features 249 experiment.Flag(f) 250 251 // Create an io.Writer that writes to our Ui properly for errors. 252 // This is kind of a hack, but it does the job. Basically: create 253 // a pipe, use a scanner to break it into lines, and output each line 254 // to the UI. Do this forever. 255 errR, errW := io.Pipe() 256 errScanner := bufio.NewScanner(errR) 257 go func() { 258 for errScanner.Scan() { 259 m.Ui.Error(errScanner.Text()) 260 } 261 }() 262 f.SetOutput(errW) 263 264 // Set the default Usage to empty 265 f.Usage = func() {} 266 267 // command that bypass locking will supply their own flag on this var, but 268 // set the initial meta value to true as a failsafe. 269 m.stateLock = true 270 271 return f 272 } 273 274 // moduleStorage returns the module.Storage implementation used to store 275 // modules for commands. 276 func (m *Meta) moduleStorage(root string) getter.Storage { 277 return &uiModuleStorage{ 278 Storage: &getter.FolderStorage{ 279 StorageDir: filepath.Join(root, "modules"), 280 }, 281 Ui: m.Ui, 282 } 283 } 284 285 // process will process the meta-parameters out of the arguments. This 286 // will potentially modify the args in-place. It will return the resulting 287 // slice. 288 // 289 // vars says whether or not we support variables. 290 func (m *Meta) process(args []string, vars bool) []string { 291 // We do this so that we retain the ability to technically call 292 // process multiple times, even if we have no plans to do so 293 if m.oldUi != nil { 294 m.Ui = m.oldUi 295 } 296 297 // Set colorization 298 m.color = m.Color 299 for i, v := range args { 300 if v == "-no-color" { 301 m.color = false 302 m.Color = false 303 args = append(args[:i], args[i+1:]...) 304 break 305 } 306 } 307 308 // Set the UI 309 m.oldUi = m.Ui 310 m.Ui = &cli.ConcurrentUi{ 311 Ui: &ColorizeUi{ 312 Colorize: m.Colorize(), 313 ErrorColor: "[red]", 314 WarnColor: "[yellow]", 315 Ui: m.oldUi, 316 }, 317 } 318 319 // If we support vars and the default var file exists, add it to 320 // the args... 321 m.autoKey = "" 322 if vars { 323 if _, err := os.Stat(DefaultVarsFilename); 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 329 } 330 331 if _, err := os.Stat(DefaultVarsFilename + ".json"); err == nil { 332 m.autoKey = "var-file-default" 333 args = append(args, "", "") 334 copy(args[2:], args[0:]) 335 args[0] = "-" + m.autoKey 336 args[1] = DefaultVarsFilename + ".json" 337 } 338 } 339 340 return args 341 } 342 343 // uiHook returns the UiHook to use with the context. 344 func (m *Meta) uiHook() *UiHook { 345 return &UiHook{ 346 Colorize: m.Colorize(), 347 Ui: m.Ui, 348 } 349 } 350 351 // confirm asks a yes/no confirmation. 352 func (m *Meta) confirm(opts *terraform.InputOpts) (bool, error) { 353 if !m.input { 354 return false, errors.New("input disabled") 355 } 356 for { 357 v, err := m.UIInput().Input(opts) 358 if err != nil { 359 return false, fmt.Errorf( 360 "Error asking for confirmation: %s", err) 361 } 362 363 switch strings.ToLower(v) { 364 case "no": 365 return false, nil 366 case "yes": 367 return true, nil 368 } 369 } 370 } 371 372 const ( 373 // ModuleDepthDefault is the default value for 374 // module depth, which can be overridden by flag 375 // or env var 376 ModuleDepthDefault = -1 377 378 // ModuleDepthEnvVar is the name of the environment variable that can be used to set module depth. 379 ModuleDepthEnvVar = "TF_MODULE_DEPTH" 380 ) 381 382 func (m *Meta) addModuleDepthFlag(flags *flag.FlagSet, moduleDepth *int) { 383 flags.IntVar(moduleDepth, "module-depth", ModuleDepthDefault, "module-depth") 384 if envVar := os.Getenv(ModuleDepthEnvVar); envVar != "" { 385 if md, err := strconv.Atoi(envVar); err == nil { 386 *moduleDepth = md 387 } 388 } 389 } 390 391 // outputShadowError outputs the error from ctx.ShadowError. If the 392 // error is nil then nothing happens. If output is false then it isn't 393 // outputted to the user (you can define logic to guard against outputting). 394 func (m *Meta) outputShadowError(err error, output bool) bool { 395 // Do nothing if no error 396 if err == nil { 397 return false 398 } 399 400 // If not outputting, do nothing 401 if !output { 402 return false 403 } 404 405 // Write the shadow error output to a file 406 path := fmt.Sprintf("terraform-error-%d.log", time.Now().UTC().Unix()) 407 if err := ioutil.WriteFile(path, []byte(err.Error()), 0644); err != nil { 408 // If there is an error writing it, just let it go 409 log.Printf("[ERROR] Error writing shadow error: %s", err) 410 return false 411 } 412 413 // Output! 414 m.Ui.Output(m.Colorize().Color(fmt.Sprintf( 415 "[reset][bold][yellow]\nExperimental feature failure! Please report a bug.\n\n"+ 416 "This is not an error. Your Terraform operation completed successfully.\n"+ 417 "Your real infrastructure is unaffected by this message.\n\n"+ 418 "[reset][yellow]While running, Terraform sometimes tests experimental features in the\n"+ 419 "background. These features cannot affect real state and never touch\n"+ 420 "real infrastructure. If the features work properly, you see nothing.\n"+ 421 "If the features fail, this message appears.\n\n"+ 422 "You can report an issue at: https://github.com/hashicorp/terraform/issues\n\n"+ 423 "The failure was written to %q. Please\n"+ 424 "double check this file contains no sensitive information and report\n"+ 425 "it with your issue.\n\n"+ 426 "This is not an error. Your terraform operation completed successfully\n"+ 427 "and your real infrastructure is unaffected by this message.", 428 path, 429 ))) 430 431 return true 432 } 433 434 // Env returns the name of the currently configured environment, corresponding 435 // to the desired named state. 436 func (m *Meta) Env() string { 437 dataDir := m.dataDir 438 if m.dataDir == "" { 439 dataDir = DefaultDataDir 440 } 441 442 envData, err := ioutil.ReadFile(filepath.Join(dataDir, local.DefaultEnvFile)) 443 current := string(bytes.TrimSpace(envData)) 444 if current == "" { 445 current = backend.DefaultStateName 446 } 447 448 if err != nil && !os.IsNotExist(err) { 449 // always return the default if we can't get an environment name 450 log.Printf("[ERROR] failed to read current environment: %s", err) 451 } 452 453 return current 454 } 455 456 // SetEnv saves the named environment to the local filesystem. 457 func (m *Meta) SetEnv(name string) error { 458 dataDir := m.dataDir 459 if m.dataDir == "" { 460 dataDir = DefaultDataDir 461 } 462 463 err := os.MkdirAll(dataDir, 0755) 464 if err != nil { 465 return err 466 } 467 468 err = ioutil.WriteFile(filepath.Join(dataDir, local.DefaultEnvFile), []byte(name), 0644) 469 if err != nil { 470 return err 471 } 472 return nil 473 }