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