github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/main.go (about)

     1  package main
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"log"
     9  	"net"
    10  	"os"
    11  	"path/filepath"
    12  	"runtime"
    13  	"strings"
    14  	"sync"
    15  
    16  	"github.com/hashicorp/go-plugin"
    17  	"github.com/hashicorp/terraform-svchost/disco"
    18  	"github.com/hashicorp/terraform/addrs"
    19  	"github.com/hashicorp/terraform/command/cliconfig"
    20  	"github.com/hashicorp/terraform/command/format"
    21  	"github.com/hashicorp/terraform/helper/logging"
    22  	"github.com/hashicorp/terraform/httpclient"
    23  	"github.com/hashicorp/terraform/version"
    24  	"github.com/mattn/go-colorable"
    25  	"github.com/mattn/go-shellwords"
    26  	"github.com/mitchellh/cli"
    27  	"github.com/mitchellh/colorstring"
    28  	"github.com/mitchellh/panicwrap"
    29  	"github.com/mitchellh/prefixedio"
    30  
    31  	backendInit "github.com/hashicorp/terraform/backend/init"
    32  )
    33  
    34  const (
    35  	// EnvCLI is the environment variable name to set additional CLI args.
    36  	EnvCLI = "TF_CLI_ARGS"
    37  )
    38  
    39  func main() {
    40  	// Override global prefix set by go-dynect during init()
    41  	log.SetPrefix("")
    42  	os.Exit(realMain())
    43  }
    44  
    45  func realMain() int {
    46  	var wrapConfig panicwrap.WrapConfig
    47  
    48  	// don't re-exec terraform as a child process for easier debugging
    49  	if os.Getenv("TF_FORK") == "0" {
    50  		return wrappedMain()
    51  	}
    52  
    53  	if !panicwrap.Wrapped(&wrapConfig) {
    54  		// Determine where logs should go in general (requested by the user)
    55  		logWriter, err := logging.LogOutput()
    56  		if err != nil {
    57  			fmt.Fprintf(os.Stderr, "Couldn't setup log output: %s", err)
    58  			return 1
    59  		}
    60  
    61  		// We always send logs to a temporary file that we use in case
    62  		// there is a panic. Otherwise, we delete it.
    63  		logTempFile, err := ioutil.TempFile("", "terraform-log")
    64  		if err != nil {
    65  			fmt.Fprintf(os.Stderr, "Couldn't setup logging tempfile: %s", err)
    66  			return 1
    67  		}
    68  		defer os.Remove(logTempFile.Name())
    69  		defer logTempFile.Close()
    70  
    71  		// Setup the prefixed readers that send data properly to
    72  		// stdout/stderr.
    73  		doneCh := make(chan struct{})
    74  		outR, outW := io.Pipe()
    75  		go copyOutput(outR, doneCh)
    76  
    77  		// Create the configuration for panicwrap and wrap our executable
    78  		wrapConfig.Handler = panicHandler(logTempFile)
    79  		wrapConfig.Writer = io.MultiWriter(logTempFile, logWriter)
    80  		wrapConfig.Stdout = outW
    81  		wrapConfig.IgnoreSignals = ignoreSignals
    82  		wrapConfig.ForwardSignals = forwardSignals
    83  		exitStatus, err := panicwrap.Wrap(&wrapConfig)
    84  		if err != nil {
    85  			fmt.Fprintf(os.Stderr, "Couldn't start Terraform: %s", err)
    86  			return 1
    87  		}
    88  
    89  		// If >= 0, we're the parent, so just exit
    90  		if exitStatus >= 0 {
    91  			// Close the stdout writer so that our copy process can finish
    92  			outW.Close()
    93  
    94  			// Wait for the output copying to finish
    95  			<-doneCh
    96  
    97  			return exitStatus
    98  		}
    99  
   100  		// We're the child, so just close the tempfile we made in order to
   101  		// save file handles since the tempfile is only used by the parent.
   102  		logTempFile.Close()
   103  	}
   104  
   105  	// Call the real main
   106  	return wrappedMain()
   107  }
   108  
   109  func init() {
   110  	Ui = &cli.PrefixedUi{
   111  		AskPrefix:    OutputPrefix,
   112  		OutputPrefix: OutputPrefix,
   113  		InfoPrefix:   OutputPrefix,
   114  		ErrorPrefix:  ErrorPrefix,
   115  		Ui: &cli.BasicUi{
   116  			Writer: os.Stdout,
   117  			Reader: os.Stdin,
   118  		},
   119  	}
   120  }
   121  
   122  func wrappedMain() int {
   123  	var err error
   124  
   125  	log.SetOutput(os.Stderr)
   126  	log.Printf(
   127  		"[INFO] Terraform version: %s %s %s",
   128  		Version, VersionPrerelease, GitCommit)
   129  	log.Printf("[INFO] Go runtime version: %s", runtime.Version())
   130  	log.Printf("[INFO] CLI args: %#v", os.Args)
   131  
   132  	config, diags := cliconfig.LoadConfig()
   133  	if len(diags) > 0 {
   134  		// Since we haven't instantiated a command.Meta yet, we need to do
   135  		// some things manually here and use some "safe" defaults for things
   136  		// that command.Meta could otherwise figure out in smarter ways.
   137  		Ui.Error("There are some problems with the CLI configuration:")
   138  		for _, diag := range diags {
   139  			earlyColor := &colorstring.Colorize{
   140  				Colors:  colorstring.DefaultColors,
   141  				Disable: true, // Disable color to be conservative until we know better
   142  				Reset:   true,
   143  			}
   144  			// We don't currently have access to the source code cache for
   145  			// the parser used to load the CLI config, so we can't show
   146  			// source code snippets in early diagnostics.
   147  			Ui.Error(format.Diagnostic(diag, nil, earlyColor, 78))
   148  		}
   149  		if diags.HasErrors() {
   150  			Ui.Error("As a result of the above problems, Terraform may not behave as intended.\n\n")
   151  			// We continue to run anyway, since Terraform has reasonable defaults.
   152  		}
   153  	}
   154  
   155  	// Get any configured credentials from the config and initialize
   156  	// a service discovery object.
   157  	credsSrc, err := credentialsSource(config)
   158  	if err != nil {
   159  		// Most commands don't actually need credentials, and most situations
   160  		// that would get us here would already have been reported by the config
   161  		// loading above, so we'll just log this one as an aid to debugging
   162  		// in the unlikely event that it _does_ arise.
   163  		log.Printf("[WARN] Cannot initialize remote host credentials manager: %s", err)
   164  		// credsSrc may be nil in this case, but that's okay because the disco
   165  		// object checks that and just acts as though no credentials are present.
   166  	}
   167  	services := disco.NewWithCredentialsSource(credsSrc)
   168  	services.SetUserAgent(httpclient.TerraformUserAgent(version.String()))
   169  
   170  	// The user can declare that certain providers are being managed on
   171  	// Terraform's behalf using this environment variable. Thsi is used
   172  	// primarily by the SDK's acceptance testing framework.
   173  	unmanagedProviders, err := parseReattachProviders(os.Getenv("TF_REATTACH_PROVIDERS"))
   174  	if err != nil {
   175  		Ui.Error(err.Error())
   176  		return 1
   177  	}
   178  
   179  	// Initialize the backends.
   180  	backendInit.Init(services)
   181  
   182  	// In tests, Commands may already be set to provide mock commands
   183  	if Commands == nil {
   184  		initCommands(config, services, unmanagedProviders)
   185  	}
   186  
   187  	// Run checkpoint
   188  	go runCheckpoint(config)
   189  
   190  	// Make sure we clean up any managed plugins at the end of this
   191  	defer plugin.CleanupClients()
   192  
   193  	// Get the command line args.
   194  	binName := filepath.Base(os.Args[0])
   195  	args := os.Args[1:]
   196  
   197  	// Build the CLI so far, we do this so we can query the subcommand.
   198  	cliRunner := &cli.CLI{
   199  		Args:       args,
   200  		Commands:   Commands,
   201  		HelpFunc:   helpFunc,
   202  		HelpWriter: os.Stdout,
   203  	}
   204  
   205  	// Prefix the args with any args from the EnvCLI
   206  	args, err = mergeEnvArgs(EnvCLI, cliRunner.Subcommand(), args)
   207  	if err != nil {
   208  		Ui.Error(err.Error())
   209  		return 1
   210  	}
   211  
   212  	// Prefix the args with any args from the EnvCLI targeting this command
   213  	suffix := strings.Replace(strings.Replace(
   214  		cliRunner.Subcommand(), "-", "_", -1), " ", "_", -1)
   215  	args, err = mergeEnvArgs(
   216  		fmt.Sprintf("%s_%s", EnvCLI, suffix), cliRunner.Subcommand(), args)
   217  	if err != nil {
   218  		Ui.Error(err.Error())
   219  		return 1
   220  	}
   221  
   222  	// We shortcut "--version" and "-v" to just show the version
   223  	for _, arg := range args {
   224  		if arg == "-v" || arg == "-version" || arg == "--version" {
   225  			newArgs := make([]string, len(args)+1)
   226  			newArgs[0] = "version"
   227  			copy(newArgs[1:], args)
   228  			args = newArgs
   229  			break
   230  		}
   231  	}
   232  
   233  	// Rebuild the CLI with any modified args.
   234  	log.Printf("[INFO] CLI command args: %#v", args)
   235  	cliRunner = &cli.CLI{
   236  		Name:       binName,
   237  		Args:       args,
   238  		Commands:   Commands,
   239  		HelpFunc:   helpFunc,
   240  		HelpWriter: os.Stdout,
   241  
   242  		Autocomplete:          true,
   243  		AutocompleteInstall:   "install-autocomplete",
   244  		AutocompleteUninstall: "uninstall-autocomplete",
   245  	}
   246  
   247  	// Pass in the overriding plugin paths from config
   248  	PluginOverrides.Providers = config.Providers
   249  	PluginOverrides.Provisioners = config.Provisioners
   250  
   251  	exitCode, err := cliRunner.Run()
   252  	if err != nil {
   253  		Ui.Error(fmt.Sprintf("Error executing CLI: %s", err.Error()))
   254  		return 1
   255  	}
   256  
   257  	return exitCode
   258  }
   259  
   260  // copyOutput uses output prefixes to determine whether data on stdout
   261  // should go to stdout or stderr. This is due to panicwrap using stderr
   262  // as the log and error channel.
   263  func copyOutput(r io.Reader, doneCh chan<- struct{}) {
   264  	defer close(doneCh)
   265  
   266  	pr, err := prefixedio.NewReader(r)
   267  	if err != nil {
   268  		panic(err)
   269  	}
   270  
   271  	stderrR, err := pr.Prefix(ErrorPrefix)
   272  	if err != nil {
   273  		panic(err)
   274  	}
   275  	stdoutR, err := pr.Prefix(OutputPrefix)
   276  	if err != nil {
   277  		panic(err)
   278  	}
   279  	defaultR, err := pr.Prefix("")
   280  	if err != nil {
   281  		panic(err)
   282  	}
   283  
   284  	var stdout io.Writer = os.Stdout
   285  	var stderr io.Writer = os.Stderr
   286  
   287  	if runtime.GOOS == "windows" {
   288  		stdout = colorable.NewColorableStdout()
   289  		stderr = colorable.NewColorableStderr()
   290  
   291  		// colorable is not concurrency-safe when stdout and stderr are the
   292  		// same console, so we need to add some synchronization to ensure that
   293  		// we can't be concurrently writing to both stderr and stdout at
   294  		// once, or else we get intermingled writes that create gibberish
   295  		// in the console.
   296  		wrapped := synchronizedWriters(stdout, stderr)
   297  		stdout = wrapped[0]
   298  		stderr = wrapped[1]
   299  	}
   300  
   301  	var wg sync.WaitGroup
   302  	wg.Add(3)
   303  	go func() {
   304  		defer wg.Done()
   305  		io.Copy(stderr, stderrR)
   306  	}()
   307  	go func() {
   308  		defer wg.Done()
   309  		io.Copy(stdout, stdoutR)
   310  	}()
   311  	go func() {
   312  		defer wg.Done()
   313  		io.Copy(stdout, defaultR)
   314  	}()
   315  
   316  	wg.Wait()
   317  }
   318  
   319  func mergeEnvArgs(envName string, cmd string, args []string) ([]string, error) {
   320  	v := os.Getenv(envName)
   321  	if v == "" {
   322  		return args, nil
   323  	}
   324  
   325  	log.Printf("[INFO] %s value: %q", envName, v)
   326  	extra, err := shellwords.Parse(v)
   327  	if err != nil {
   328  		return nil, fmt.Errorf(
   329  			"Error parsing extra CLI args from %s: %s",
   330  			envName, err)
   331  	}
   332  
   333  	// Find the command to look for in the args. If there is a space,
   334  	// we need to find the last part.
   335  	search := cmd
   336  	if idx := strings.LastIndex(search, " "); idx >= 0 {
   337  		search = cmd[idx+1:]
   338  	}
   339  
   340  	// Find the index to place the flags. We put them exactly
   341  	// after the first non-flag arg.
   342  	idx := -1
   343  	for i, v := range args {
   344  		if v == search {
   345  			idx = i
   346  			break
   347  		}
   348  	}
   349  
   350  	// idx points to the exact arg that isn't a flag. We increment
   351  	// by one so that all the copying below expects idx to be the
   352  	// insertion point.
   353  	idx++
   354  
   355  	// Copy the args
   356  	newArgs := make([]string, len(args)+len(extra))
   357  	copy(newArgs, args[:idx])
   358  	copy(newArgs[idx:], extra)
   359  	copy(newArgs[len(extra)+idx:], args[idx:])
   360  	return newArgs, nil
   361  }
   362  
   363  // parse information on reattaching to unmanaged providers out of a
   364  // JSON-encoded environment variable.
   365  func parseReattachProviders(in string) (map[addrs.Provider]*plugin.ReattachConfig, error) {
   366  	unmanagedProviders := map[addrs.Provider]*plugin.ReattachConfig{}
   367  	if in != "" {
   368  		type reattachConfig struct {
   369  			Protocol string
   370  			Addr     struct {
   371  				Network string
   372  				String  string
   373  			}
   374  			Pid  int
   375  			Test bool
   376  		}
   377  		var m map[string]reattachConfig
   378  		err := json.Unmarshal([]byte(in), &m)
   379  		if err != nil {
   380  			return unmanagedProviders, fmt.Errorf("Invalid format for TF_REATTACH_PROVIDERS: %s", err)
   381  		}
   382  		for p, c := range m {
   383  			a, diags := addrs.ParseProviderSourceString(p)
   384  			if diags.HasErrors() {
   385  				return unmanagedProviders, fmt.Errorf("Error parsing %q as a provider address: %s", a, diags.Err())
   386  			}
   387  			var addr net.Addr
   388  			switch c.Addr.Network {
   389  			case "unix":
   390  				addr, err = net.ResolveUnixAddr("unix", c.Addr.String)
   391  				if err != nil {
   392  					return unmanagedProviders, fmt.Errorf("Invalid unix socket path %q for %q: %s", c.Addr.String, p, err)
   393  				}
   394  			case "tcp":
   395  				addr, err = net.ResolveTCPAddr("tcp", c.Addr.String)
   396  				if err != nil {
   397  					return unmanagedProviders, fmt.Errorf("Invalid TCP address %q for %q: %s", c.Addr.String, p, err)
   398  				}
   399  			default:
   400  				return unmanagedProviders, fmt.Errorf("Unknown address type %q for %q", c.Addr.Network, p)
   401  			}
   402  			unmanagedProviders[a] = &plugin.ReattachConfig{
   403  				Protocol: plugin.Protocol(c.Protocol),
   404  				Pid:      c.Pid,
   405  				Test:     c.Test,
   406  				Addr:     addr,
   407  			}
   408  		}
   409  	}
   410  	return unmanagedProviders, nil
   411  }