github.com/posener/terraform@v0.11.0-beta1.0.20171103235147-645df36af025/main.go (about) 1 package main 2 3 import ( 4 "fmt" 5 "io" 6 "io/ioutil" 7 "log" 8 "os" 9 "path/filepath" 10 "runtime" 11 "strings" 12 "sync" 13 14 "github.com/mitchellh/colorstring" 15 16 "github.com/hashicorp/go-plugin" 17 "github.com/hashicorp/terraform/command/format" 18 "github.com/hashicorp/terraform/helper/logging" 19 "github.com/hashicorp/terraform/terraform" 20 "github.com/mattn/go-colorable" 21 "github.com/mattn/go-shellwords" 22 "github.com/mitchellh/cli" 23 "github.com/mitchellh/panicwrap" 24 "github.com/mitchellh/prefixedio" 25 ) 26 27 const ( 28 // EnvCLI is the environment variable name to set additional CLI args. 29 EnvCLI = "TF_CLI_ARGS" 30 ) 31 32 func main() { 33 // Override global prefix set by go-dynect during init() 34 log.SetPrefix("") 35 os.Exit(realMain()) 36 } 37 38 func realMain() int { 39 var wrapConfig panicwrap.WrapConfig 40 41 // don't re-exec terraform as a child process for easier debugging 42 if os.Getenv("TF_FORK") == "0" { 43 return wrappedMain() 44 } 45 46 if !panicwrap.Wrapped(&wrapConfig) { 47 // Determine where logs should go in general (requested by the user) 48 logWriter, err := logging.LogOutput() 49 if err != nil { 50 fmt.Fprintf(os.Stderr, "Couldn't setup log output: %s", err) 51 return 1 52 } 53 54 // We always send logs to a temporary file that we use in case 55 // there is a panic. Otherwise, we delete it. 56 logTempFile, err := ioutil.TempFile("", "terraform-log") 57 if err != nil { 58 fmt.Fprintf(os.Stderr, "Couldn't setup logging tempfile: %s", err) 59 return 1 60 } 61 defer os.Remove(logTempFile.Name()) 62 defer logTempFile.Close() 63 64 // Setup the prefixed readers that send data properly to 65 // stdout/stderr. 66 doneCh := make(chan struct{}) 67 outR, outW := io.Pipe() 68 go copyOutput(outR, doneCh) 69 70 // Create the configuration for panicwrap and wrap our executable 71 wrapConfig.Handler = panicHandler(logTempFile) 72 wrapConfig.Writer = io.MultiWriter(logTempFile, logWriter) 73 wrapConfig.Stdout = outW 74 wrapConfig.IgnoreSignals = ignoreSignals 75 wrapConfig.ForwardSignals = forwardSignals 76 exitStatus, err := panicwrap.Wrap(&wrapConfig) 77 if err != nil { 78 fmt.Fprintf(os.Stderr, "Couldn't start Terraform: %s", err) 79 return 1 80 } 81 82 // If >= 0, we're the parent, so just exit 83 if exitStatus >= 0 { 84 // Close the stdout writer so that our copy process can finish 85 outW.Close() 86 87 // Wait for the output copying to finish 88 <-doneCh 89 90 return exitStatus 91 } 92 93 // We're the child, so just close the tempfile we made in order to 94 // save file handles since the tempfile is only used by the parent. 95 logTempFile.Close() 96 } 97 98 // Call the real main 99 return wrappedMain() 100 } 101 102 func init() { 103 Ui = &cli.PrefixedUi{ 104 AskPrefix: OutputPrefix, 105 OutputPrefix: OutputPrefix, 106 InfoPrefix: OutputPrefix, 107 ErrorPrefix: ErrorPrefix, 108 Ui: &cli.BasicUi{Writer: os.Stdout}, 109 } 110 } 111 112 func wrappedMain() int { 113 var err error 114 115 // We always need to close the DebugInfo before we exit. 116 defer terraform.CloseDebugInfo() 117 118 log.SetOutput(os.Stderr) 119 log.Printf( 120 "[INFO] Terraform version: %s %s %s", 121 Version, VersionPrerelease, GitCommit) 122 log.Printf("[INFO] Go runtime version: %s", runtime.Version()) 123 log.Printf("[INFO] CLI args: %#v", os.Args) 124 125 config, diags := LoadConfig() 126 if len(diags) > 0 { 127 // Since we haven't instantiated a command.Meta yet, we need to do 128 // some things manually here and use some "safe" defaults for things 129 // that command.Meta could otherwise figure out in smarter ways. 130 Ui.Error("There are some problems with the CLI configuration:") 131 for _, diag := range diags { 132 earlyColor := &colorstring.Colorize{ 133 Colors: colorstring.DefaultColors, 134 Disable: true, // Disable color to be conservative until we know better 135 Reset: true, 136 } 137 Ui.Error(format.Diagnostic(diag, earlyColor, 78)) 138 } 139 if diags.HasErrors() { 140 Ui.Error("As a result of the above problems, Terraform may not behave as intended.\n\n") 141 // We continue to run anyway, since Terraform has reasonable defaults. 142 } 143 } 144 log.Printf("[DEBUG] CLI config is %#v", config) 145 146 // In tests, Commands may already be set to provide mock commands 147 if Commands == nil { 148 initCommands(config) 149 } 150 151 // Run checkpoint 152 go runCheckpoint(config) 153 154 // Make sure we clean up any managed plugins at the end of this 155 defer plugin.CleanupClients() 156 157 // Get the command line args. 158 binName := filepath.Base(os.Args[0]) 159 args := os.Args[1:] 160 161 // Build the CLI so far, we do this so we can query the subcommand. 162 cliRunner := &cli.CLI{ 163 Args: args, 164 Commands: Commands, 165 HelpFunc: helpFunc, 166 HelpWriter: os.Stdout, 167 } 168 169 // Prefix the args with any args from the EnvCLI 170 args, err = mergeEnvArgs(EnvCLI, cliRunner.Subcommand(), args) 171 if err != nil { 172 Ui.Error(err.Error()) 173 return 1 174 } 175 176 // Prefix the args with any args from the EnvCLI targeting this command 177 suffix := strings.Replace(strings.Replace( 178 cliRunner.Subcommand(), "-", "_", -1), " ", "_", -1) 179 args, err = mergeEnvArgs( 180 fmt.Sprintf("%s_%s", EnvCLI, suffix), cliRunner.Subcommand(), args) 181 if err != nil { 182 Ui.Error(err.Error()) 183 return 1 184 } 185 186 // We shortcut "--version" and "-v" to just show the version 187 for _, arg := range args { 188 if arg == "-v" || arg == "-version" || arg == "--version" { 189 newArgs := make([]string, len(args)+1) 190 newArgs[0] = "version" 191 copy(newArgs[1:], args) 192 args = newArgs 193 break 194 } 195 } 196 197 // Rebuild the CLI with any modified args. 198 log.Printf("[INFO] CLI command args: %#v", args) 199 cliRunner = &cli.CLI{ 200 Name: binName, 201 Args: args, 202 Commands: Commands, 203 HelpFunc: helpFunc, 204 HelpWriter: os.Stdout, 205 206 Autocomplete: true, 207 AutocompleteInstall: "install-autocomplete", 208 AutocompleteUninstall: "uninstall-autocomplete", 209 } 210 211 // Pass in the overriding plugin paths from config 212 PluginOverrides.Providers = config.Providers 213 PluginOverrides.Provisioners = config.Provisioners 214 215 exitCode, err := cliRunner.Run() 216 if err != nil { 217 Ui.Error(fmt.Sprintf("Error executing CLI: %s", err.Error())) 218 return 1 219 } 220 221 return exitCode 222 } 223 224 func cliConfigFile() (string, error) { 225 mustExist := true 226 configFilePath := os.Getenv("TERRAFORM_CONFIG") 227 if configFilePath == "" { 228 var err error 229 configFilePath, err = ConfigFile() 230 mustExist = false 231 232 if err != nil { 233 log.Printf( 234 "[ERROR] Error detecting default CLI config file path: %s", 235 err) 236 } 237 } 238 239 log.Printf("[DEBUG] Attempting to open CLI config file: %s", configFilePath) 240 f, err := os.Open(configFilePath) 241 if err == nil { 242 f.Close() 243 return configFilePath, nil 244 } 245 246 if mustExist || !os.IsNotExist(err) { 247 return "", err 248 } 249 250 log.Println("[DEBUG] File doesn't exist, but doesn't need to. Ignoring.") 251 return "", nil 252 } 253 254 // copyOutput uses output prefixes to determine whether data on stdout 255 // should go to stdout or stderr. This is due to panicwrap using stderr 256 // as the log and error channel. 257 func copyOutput(r io.Reader, doneCh chan<- struct{}) { 258 defer close(doneCh) 259 260 pr, err := prefixedio.NewReader(r) 261 if err != nil { 262 panic(err) 263 } 264 265 stderrR, err := pr.Prefix(ErrorPrefix) 266 if err != nil { 267 panic(err) 268 } 269 stdoutR, err := pr.Prefix(OutputPrefix) 270 if err != nil { 271 panic(err) 272 } 273 defaultR, err := pr.Prefix("") 274 if err != nil { 275 panic(err) 276 } 277 278 var stdout io.Writer = os.Stdout 279 var stderr io.Writer = os.Stderr 280 281 if runtime.GOOS == "windows" { 282 stdout = colorable.NewColorableStdout() 283 stderr = colorable.NewColorableStderr() 284 285 // colorable is not concurrency-safe when stdout and stderr are the 286 // same console, so we need to add some synchronization to ensure that 287 // we can't be concurrently writing to both stderr and stdout at 288 // once, or else we get intermingled writes that create gibberish 289 // in the console. 290 wrapped := synchronizedWriters(stdout, stderr) 291 stdout = wrapped[0] 292 stderr = wrapped[1] 293 } 294 295 var wg sync.WaitGroup 296 wg.Add(3) 297 go func() { 298 defer wg.Done() 299 io.Copy(stderr, stderrR) 300 }() 301 go func() { 302 defer wg.Done() 303 io.Copy(stdout, stdoutR) 304 }() 305 go func() { 306 defer wg.Done() 307 io.Copy(stdout, defaultR) 308 }() 309 310 wg.Wait() 311 } 312 313 func mergeEnvArgs(envName string, cmd string, args []string) ([]string, error) { 314 v := os.Getenv(envName) 315 if v == "" { 316 return args, nil 317 } 318 319 log.Printf("[INFO] %s value: %q", envName, v) 320 extra, err := shellwords.Parse(v) 321 if err != nil { 322 return nil, fmt.Errorf( 323 "Error parsing extra CLI args from %s: %s", 324 envName, err) 325 } 326 327 // Find the command to look for in the args. If there is a space, 328 // we need to find the last part. 329 search := cmd 330 if idx := strings.LastIndex(search, " "); idx >= 0 { 331 search = cmd[idx+1:] 332 } 333 334 // Find the index to place the flags. We put them exactly 335 // after the first non-flag arg. 336 idx := -1 337 for i, v := range args { 338 if v == search { 339 idx = i 340 break 341 } 342 } 343 344 // idx points to the exact arg that isn't a flag. We increment 345 // by one so that all the copying below expects idx to be the 346 // insertion point. 347 idx++ 348 349 // Copy the args 350 newArgs := make([]string, len(args)+len(extra)) 351 copy(newArgs, args[:idx]) 352 copy(newArgs[idx:], extra) 353 copy(newArgs[len(extra)+idx:], args[idx:]) 354 return newArgs, nil 355 }