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  }