package main import ( "fmt" "io" "io/ioutil" "log" "os" "runtime" "strings" "sync" "github.com/hashicorp/go-plugin" "github.com/hashicorp/terraform/helper/logging" "github.com/hashicorp/terraform/terraform" "github.com/mattn/go-colorable" "github.com/mattn/go-shellwords" "github.com/mitchellh/cli" "github.com/mitchellh/panicwrap" "github.com/mitchellh/prefixedio" ) const ( // EnvCLI is the environment variable name to set additional CLI args. EnvCLI = "TF_CLI_ARGS" ) func main() { // Override global prefix set by go-dynect during init() log.SetPrefix("") os.Exit(realMain()) } func realMain() int { var wrapConfig panicwrap.WrapConfig // don't re-exec terraform as a child process for easier debugging if os.Getenv("TF_FORK") == "0" { return wrappedMain() } if !panicwrap.Wrapped(&wrapConfig) { // Determine where logs should go in general (requested by the user) logWriter, err := logging.LogOutput() if err != nil { fmt.Fprintf(os.Stderr, "Couldn't setup log output: %s", err) return 1 } // We always send logs to a temporary file that we use in case // there is a panic. Otherwise, we delete it. logTempFile, err := ioutil.TempFile("", "terraform-log") if err != nil { fmt.Fprintf(os.Stderr, "Couldn't setup logging tempfile: %s", err) return 1 } defer os.Remove(logTempFile.Name()) defer logTempFile.Close() // Setup the prefixed readers that send data properly to // stdout/stderr. doneCh := make(chan struct{}) outR, outW := io.Pipe() go copyOutput(outR, doneCh) // Create the configuration for panicwrap and wrap our executable wrapConfig.Handler = panicHandler(logTempFile) wrapConfig.Writer = io.MultiWriter(logTempFile, logWriter) wrapConfig.Stdout = outW wrapConfig.IgnoreSignals = ignoreSignals wrapConfig.ForwardSignals = forwardSignals exitStatus, err := panicwrap.Wrap(&wrapConfig) if err != nil { fmt.Fprintf(os.Stderr, "Couldn't start Terraform: %s", err) return 1 } // If >= 0, we're the parent, so just exit if exitStatus >= 0 { // Close the stdout writer so that our copy process can finish outW.Close() // Wait for the output copying to finish <-doneCh return exitStatus } // We're the child, so just close the tempfile we made in order to // save file handles since the tempfile is only used by the parent. logTempFile.Close() } // Call the real main return wrappedMain() } func wrappedMain() int { // We always need to close the DebugInfo before we exit. defer terraform.CloseDebugInfo() log.SetOutput(os.Stderr) log.Printf( "[INFO] Terraform version: %s %s %s", Version, VersionPrerelease, GitCommit) log.Printf("[INFO] CLI args: %#v", os.Args) // Load the configuration config := BuiltinConfig if err := config.Discover(Ui); err != nil { Ui.Error(fmt.Sprintf("Error discovering plugins: %s", err)) return 1 } // Load the configuration file if we have one, that can be used to // define extra providers and provisioners. clicfgFile, err := cliConfigFile() if err != nil { Ui.Error(fmt.Sprintf("Error loading CLI configuration: \n\n%s", err)) return 1 } if clicfgFile != "" { usrcfg, err := LoadConfig(clicfgFile) if err != nil { Ui.Error(fmt.Sprintf("Error loading CLI configuration: \n\n%s", err)) return 1 } config = *config.Merge(usrcfg) } // Run checkpoint go runCheckpoint(&config) // Make sure we clean up any managed plugins at the end of this defer plugin.CleanupClients() // Get the command line args. args := os.Args[1:] // Build the CLI so far, we do this so we can query the subcommand. cliRunner := &cli.CLI{ Args: args, Commands: Commands, HelpFunc: helpFunc, HelpWriter: os.Stdout, } // Prefix the args with any args from the EnvCLI args, err = mergeEnvArgs(EnvCLI, args) if err != nil { Ui.Error(err.Error()) return 1 } // Prefix the args with any args from the EnvCLI targeting this command suffix := strings.Replace(cliRunner.Subcommand(), "-", "_", -1) args, err = mergeEnvArgs(fmt.Sprintf("%s_%s", EnvCLI, suffix), args) if err != nil { Ui.Error(err.Error()) return 1 } // We shortcut "--version" and "-v" to just show the version for _, arg := range args { if arg == "-v" || arg == "-version" || arg == "--version" { newArgs := make([]string, len(args)+1) newArgs[0] = "version" copy(newArgs[1:], args) args = newArgs break } } // Rebuild the CLI with any modified args. log.Printf("[INFO] CLI command args: %#v", args) cliRunner = &cli.CLI{ Args: args, Commands: Commands, HelpFunc: helpFunc, HelpWriter: os.Stdout, } // Initialize the TFConfig settings for the commands... ContextOpts.Providers = config.ProviderFactories() ContextOpts.Provisioners = config.ProvisionerFactories() exitCode, err := cliRunner.Run() if err != nil { Ui.Error(fmt.Sprintf("Error executing CLI: %s", err.Error())) return 1 } return exitCode } func cliConfigFile() (string, error) { mustExist := true configFilePath := os.Getenv("TERRAFORM_CONFIG") if configFilePath == "" { var err error configFilePath, err = ConfigFile() mustExist = false if err != nil { log.Printf( "[ERROR] Error detecting default CLI config file path: %s", err) } } log.Printf("[DEBUG] Attempting to open CLI config file: %s", configFilePath) f, err := os.Open(configFilePath) if err == nil { f.Close() return configFilePath, nil } if mustExist || !os.IsNotExist(err) { return "", err } log.Println("[DEBUG] File doesn't exist, but doesn't need to. Ignoring.") return "", nil } // copyOutput uses output prefixes to determine whether data on stdout // should go to stdout or stderr. This is due to panicwrap using stderr // as the log and error channel. func copyOutput(r io.Reader, doneCh chan<- struct{}) { defer close(doneCh) pr, err := prefixedio.NewReader(r) if err != nil { panic(err) } stderrR, err := pr.Prefix(ErrorPrefix) if err != nil { panic(err) } stdoutR, err := pr.Prefix(OutputPrefix) if err != nil { panic(err) } defaultR, err := pr.Prefix("") if err != nil { panic(err) } var stdout io.Writer = os.Stdout var stderr io.Writer = os.Stderr if runtime.GOOS == "windows" { stdout = colorable.NewColorableStdout() stderr = colorable.NewColorableStderr() } var wg sync.WaitGroup wg.Add(3) go func() { defer wg.Done() io.Copy(stderr, stderrR) }() go func() { defer wg.Done() io.Copy(stdout, stdoutR) }() go func() { defer wg.Done() io.Copy(stdout, defaultR) }() wg.Wait() } func mergeEnvArgs(envName string, args []string) ([]string, error) { v := os.Getenv(envName) if v == "" { return args, nil } log.Printf("[INFO] %s value: %q", envName, v) extra, err := shellwords.Parse(v) if err != nil { return nil, fmt.Errorf( "Error parsing extra CLI args from %s: %s", envName, err) } // Find the index to place the flags. We put them exactly // after the first non-flag arg. idx := -1 for i, v := range args { if len(v) > 0 && v[0] != '-' { idx = i break } } // idx points to the exact arg that isn't a flag. We increment // by one so that all the copying below expects idx to be the // insertion point. idx++ // Copy the args newArgs := make([]string, len(args)+len(extra)) copy(newArgs, args[:idx]) copy(newArgs[idx:], extra) copy(newArgs[len(extra)+idx:], args[idx:]) return newArgs, nil }