github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/main.go (about)

     1  package main
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"log"
     8  	"net"
     9  	"os"
    10  	"path/filepath"
    11  	"runtime"
    12  	"strings"
    13  
    14  	"github.com/hashicorp/go-plugin"
    15  	"github.com/iaas-resource-provision/iaas-rpc-svchost/disco"
    16  	"github.com/iaas-resource-provision/iaas-rpc/internal/addrs"
    17  	"github.com/iaas-resource-provision/iaas-rpc/internal/command/cliconfig"
    18  	"github.com/iaas-resource-provision/iaas-rpc/internal/command/format"
    19  	"github.com/iaas-resource-provision/iaas-rpc/internal/didyoumean"
    20  	"github.com/iaas-resource-provision/iaas-rpc/internal/httpclient"
    21  	"github.com/iaas-resource-provision/iaas-rpc/internal/logging"
    22  	"github.com/iaas-resource-provision/iaas-rpc/internal/terminal"
    23  	"github.com/iaas-resource-provision/iaas-rpc/version"
    24  	"github.com/mattn/go-shellwords"
    25  	"github.com/mitchellh/cli"
    26  	"github.com/mitchellh/colorstring"
    27  	"github.com/mitchellh/panicwrap"
    28  
    29  	backendInit "github.com/iaas-resource-provision/iaas-rpc/internal/backend/init"
    30  )
    31  
    32  const (
    33  	// EnvCLI is the environment variable name to set additional CLI args.
    34  	EnvCLI = "TF_CLI_ARGS"
    35  
    36  	// The parent process will create a file to collect crash logs
    37  	envTmpLogPath = "TF_TEMP_LOG_PATH"
    38  
    39  	// Environment variable name used for smuggling true stderr terminal
    40  	// settings into a panicwrap child process. This is an implementation
    41  	// detail, subject to change in future, and should not ever be directly
    42  	// set by an end-user.
    43  	envTerminalPanicwrapWorkaround = "TF_PANICWRAP_STDERR"
    44  )
    45  
    46  // ui wraps the primary output cli.Ui, and redirects Warn calls to Output
    47  // calls. This ensures that warnings are sent to stdout, and are properly
    48  // serialized within the stdout stream.
    49  type ui struct {
    50  	cli.Ui
    51  }
    52  
    53  func (u *ui) Warn(msg string) {
    54  	u.Ui.Output(msg)
    55  }
    56  
    57  func main() {
    58  	os.Exit(realMain())
    59  }
    60  
    61  func realMain() int {
    62  	var wrapConfig panicwrap.WrapConfig
    63  
    64  	// don't re-exec terraform as a child process for easier debugging
    65  	if os.Getenv("TF_FORK") == "0" {
    66  		return wrappedMain()
    67  	}
    68  
    69  	if !panicwrap.Wrapped(&wrapConfig) {
    70  		// We always send logs to a temporary file that we use in case
    71  		// there is a panic. Otherwise, we delete it.
    72  		logTempFile, err := ioutil.TempFile("", "terraform-log")
    73  		if err != nil {
    74  			fmt.Fprintf(os.Stderr, "Couldn't set up logging tempfile: %s", err)
    75  			return 1
    76  		}
    77  		// Now that we have the file, close it and leave it for the wrapped
    78  		// process to write to.
    79  		logTempFile.Close()
    80  		defer os.Remove(logTempFile.Name())
    81  
    82  		// store the path in the environment for the wrapped executable
    83  		os.Setenv(envTmpLogPath, logTempFile.Name())
    84  
    85  		// We also need to do our terminal initialization before we fork,
    86  		// because the child process doesn't necessarily have access to
    87  		// the true stderr in order to initialize it.
    88  		streams, err := terminal.Init()
    89  		if err != nil {
    90  			fmt.Fprintf(os.Stderr, "Failed to initialize terminal: %s", err)
    91  			return 1
    92  		}
    93  
    94  		// We need the child process to behave _as if_ connected to the real
    95  		// stderr, even though panicwrap is about to add a pipe in the way,
    96  		// so we'll smuggle the true stderr information in an environment
    97  		// varible.
    98  		streamState := streams.StateForAfterPanicWrap()
    99  		os.Setenv(envTerminalPanicwrapWorkaround, fmt.Sprintf("%t:%d", streamState.StderrIsTerminal, streamState.StderrWidth))
   100  
   101  		// Create the configuration for panicwrap and wrap our executable
   102  		wrapConfig.Handler = logging.PanicHandler(logTempFile.Name())
   103  		wrapConfig.IgnoreSignals = ignoreSignals
   104  		wrapConfig.ForwardSignals = forwardSignals
   105  		exitStatus, err := panicwrap.Wrap(&wrapConfig)
   106  		if err != nil {
   107  			fmt.Fprintf(os.Stderr, "Couldn't start Terraform: %s", err)
   108  			return 1
   109  		}
   110  
   111  		return exitStatus
   112  	}
   113  
   114  	// Call the real main
   115  	return wrappedMain()
   116  }
   117  
   118  func init() {
   119  	Ui = &ui{&cli.BasicUi{
   120  		Writer:      os.Stdout,
   121  		ErrorWriter: os.Stderr,
   122  		Reader:      os.Stdin,
   123  	}}
   124  }
   125  
   126  func wrappedMain() int {
   127  	var err error
   128  
   129  	tmpLogPath := os.Getenv(envTmpLogPath)
   130  	if tmpLogPath != "" {
   131  		f, err := os.OpenFile(tmpLogPath, os.O_RDWR|os.O_APPEND, 0666)
   132  		if err == nil {
   133  			defer f.Close()
   134  
   135  			log.Printf("[DEBUG] Adding temp file log sink: %s", f.Name())
   136  			logging.RegisterSink(f)
   137  		} else {
   138  			log.Printf("[ERROR] Could not open temp log file: %v", err)
   139  		}
   140  	}
   141  
   142  	log.Printf(
   143  		"[INFO] Terraform version: %s %s",
   144  		Version, VersionPrerelease)
   145  	log.Printf("[INFO] Go runtime version: %s", runtime.Version())
   146  	log.Printf("[INFO] CLI args: %#v", os.Args)
   147  
   148  	// This is the recieving end of our workaround to retain the metadata
   149  	// about the real stderr even though we're talking to it via the panicwrap
   150  	// pipe. See the call to StateForAfterPanicWrap above for the producer
   151  	// part of this.
   152  	var streamState *terminal.PrePanicwrapState
   153  	if raw := os.Getenv(envTerminalPanicwrapWorkaround); raw != "" {
   154  		streamState = &terminal.PrePanicwrapState{}
   155  		if _, err := fmt.Sscanf(raw, "%t:%d", &streamState.StderrIsTerminal, &streamState.StderrWidth); err != nil {
   156  			log.Printf("[WARN] %s is set but is incorrectly-formatted: %s", envTerminalPanicwrapWorkaround, err)
   157  			streamState = nil // leave it unset for a normal init, then
   158  		}
   159  	}
   160  	streams, err := terminal.ReinitInsidePanicwrap(streamState)
   161  	if err != nil {
   162  		Ui.Error(fmt.Sprintf("Failed to configure the terminal: %s", err))
   163  		return 1
   164  	}
   165  	if streams.Stdout.IsTerminal() {
   166  		log.Printf("[TRACE] Stdout is a terminal of width %d", streams.Stdout.Columns())
   167  	} else {
   168  		log.Printf("[TRACE] Stdout is not a terminal")
   169  	}
   170  	if streams.Stderr.IsTerminal() {
   171  		log.Printf("[TRACE] Stderr is a terminal of width %d", streams.Stderr.Columns())
   172  	} else {
   173  		log.Printf("[TRACE] Stderr is not a terminal")
   174  	}
   175  	if streams.Stdin.IsTerminal() {
   176  		log.Printf("[TRACE] Stdin is a terminal")
   177  	} else {
   178  		log.Printf("[TRACE] Stdin is not a terminal")
   179  	}
   180  
   181  	// NOTE: We're intentionally calling LoadConfig _before_ handling a possible
   182  	// -chdir=... option on the command line, so that a possible relative
   183  	// path in the TERRAFORM_CONFIG_FILE environment variable (though probably
   184  	// ill-advised) will be resolved relative to the true working directory,
   185  	// not the overridden one.
   186  	config, diags := cliconfig.LoadConfig()
   187  
   188  	if len(diags) > 0 {
   189  		// Since we haven't instantiated a command.Meta yet, we need to do
   190  		// some things manually here and use some "safe" defaults for things
   191  		// that command.Meta could otherwise figure out in smarter ways.
   192  		Ui.Error("There are some problems with the CLI configuration:")
   193  		for _, diag := range diags {
   194  			earlyColor := &colorstring.Colorize{
   195  				Colors:  colorstring.DefaultColors,
   196  				Disable: true, // Disable color to be conservative until we know better
   197  				Reset:   true,
   198  			}
   199  			// We don't currently have access to the source code cache for
   200  			// the parser used to load the CLI config, so we can't show
   201  			// source code snippets in early diagnostics.
   202  			Ui.Error(format.Diagnostic(diag, nil, earlyColor, 78))
   203  		}
   204  		if diags.HasErrors() {
   205  			Ui.Error("As a result of the above problems, Terraform may not behave as intended.\n\n")
   206  			// We continue to run anyway, since Terraform has reasonable defaults.
   207  		}
   208  	}
   209  
   210  	// Get any configured credentials from the config and initialize
   211  	// a service discovery object. The slightly awkward predeclaration of
   212  	// disco is required to allow us to pass untyped nil as the creds source
   213  	// when creating the source fails. Otherwise we pass a typed nil which
   214  	// breaks the nil checks in the disco object
   215  	var services *disco.Disco
   216  	credsSrc, err := credentialsSource(config)
   217  	if err == nil {
   218  		services = disco.NewWithCredentialsSource(credsSrc)
   219  	} else {
   220  		// Most commands don't actually need credentials, and most situations
   221  		// that would get us here would already have been reported by the config
   222  		// loading above, so we'll just log this one as an aid to debugging
   223  		// in the unlikely event that it _does_ arise.
   224  		log.Printf("[WARN] Cannot initialize remote host credentials manager: %s", err)
   225  		// passing (untyped) nil as the creds source is okay because the disco
   226  		// object checks that and just acts as though no credentials are present.
   227  		services = disco.NewWithCredentialsSource(nil)
   228  	}
   229  	services.SetUserAgent(httpclient.TerraformUserAgent(version.String()))
   230  
   231  	providerSrc, diags := providerSource(config.ProviderInstallation, services)
   232  	if len(diags) > 0 {
   233  		Ui.Error("There are some problems with the provider_installation configuration:")
   234  		for _, diag := range diags {
   235  			earlyColor := &colorstring.Colorize{
   236  				Colors:  colorstring.DefaultColors,
   237  				Disable: true, // Disable color to be conservative until we know better
   238  				Reset:   true,
   239  			}
   240  			Ui.Error(format.Diagnostic(diag, nil, earlyColor, 78))
   241  		}
   242  		if diags.HasErrors() {
   243  			Ui.Error("As a result of the above problems, Terraform's provider installer may not behave as intended.\n\n")
   244  			// We continue to run anyway, because most commands don't do provider installation.
   245  		}
   246  	}
   247  	providerDevOverrides := providerDevOverrides(config.ProviderInstallation)
   248  
   249  	// The user can declare that certain providers are being managed on
   250  	// Terraform's behalf using this environment variable. This is used
   251  	// primarily by the SDK's acceptance testing framework.
   252  	unmanagedProviders, err := parseReattachProviders(os.Getenv("TF_REATTACH_PROVIDERS"))
   253  	if err != nil {
   254  		Ui.Error(err.Error())
   255  		return 1
   256  	}
   257  
   258  	// Initialize the backends.
   259  	backendInit.Init(services)
   260  
   261  	// Get the command line args.
   262  	binName := filepath.Base(os.Args[0])
   263  	args := os.Args[1:]
   264  
   265  	originalWd, err := os.Getwd()
   266  	if err != nil {
   267  		// It would be very strange to end up here
   268  		Ui.Error(fmt.Sprintf("Failed to determine current working directory: %s", err))
   269  		return 1
   270  	}
   271  
   272  	// The arguments can begin with a -chdir option to ask Terraform to switch
   273  	// to a different working directory for the rest of its work. If that
   274  	// option is present then extractChdirOption returns a trimmed args with that option removed.
   275  	overrideWd, args, err := extractChdirOption(args)
   276  	if err != nil {
   277  		Ui.Error(fmt.Sprintf("Invalid -chdir option: %s", err))
   278  		return 1
   279  	}
   280  	if overrideWd != "" {
   281  		err := os.Chdir(overrideWd)
   282  		if err != nil {
   283  			Ui.Error(fmt.Sprintf("Error handling -chdir option: %s", err))
   284  			return 1
   285  		}
   286  	}
   287  
   288  	// In tests, Commands may already be set to provide mock commands
   289  	if Commands == nil {
   290  		// Commands get to hold on to the original working directory here,
   291  		// in case they need to refer back to it for any special reason, though
   292  		// they should primarily be working with the override working directory
   293  		// that we've now switched to above.
   294  		initCommands(originalWd, streams, config, services, providerSrc, providerDevOverrides, unmanagedProviders)
   295  	}
   296  
   297  	// Run checkpoint
   298  	go runCheckpoint(config)
   299  
   300  	// Make sure we clean up any managed plugins at the end of this
   301  	defer plugin.CleanupClients()
   302  
   303  	// Build the CLI so far, we do this so we can query the subcommand.
   304  	cliRunner := &cli.CLI{
   305  		Args:       args,
   306  		Commands:   Commands,
   307  		HelpFunc:   helpFunc,
   308  		HelpWriter: os.Stdout,
   309  	}
   310  
   311  	// Prefix the args with any args from the EnvCLI
   312  	args, err = mergeEnvArgs(EnvCLI, cliRunner.Subcommand(), args)
   313  	if err != nil {
   314  		Ui.Error(err.Error())
   315  		return 1
   316  	}
   317  
   318  	// Prefix the args with any args from the EnvCLI targeting this command
   319  	suffix := strings.Replace(strings.Replace(
   320  		cliRunner.Subcommand(), "-", "_", -1), " ", "_", -1)
   321  	args, err = mergeEnvArgs(
   322  		fmt.Sprintf("%s_%s", EnvCLI, suffix), cliRunner.Subcommand(), args)
   323  	if err != nil {
   324  		Ui.Error(err.Error())
   325  		return 1
   326  	}
   327  
   328  	// We shortcut "--version" and "-v" to just show the version
   329  	for _, arg := range args {
   330  		if arg == "-v" || arg == "-version" || arg == "--version" {
   331  			newArgs := make([]string, len(args)+1)
   332  			newArgs[0] = "version"
   333  			copy(newArgs[1:], args)
   334  			args = newArgs
   335  			break
   336  		}
   337  	}
   338  
   339  	// Rebuild the CLI with any modified args.
   340  	log.Printf("[INFO] CLI command args: %#v", args)
   341  	cliRunner = &cli.CLI{
   342  		Name:       binName,
   343  		Args:       args,
   344  		Commands:   Commands,
   345  		HelpFunc:   helpFunc,
   346  		HelpWriter: os.Stdout,
   347  
   348  		Autocomplete:          true,
   349  		AutocompleteInstall:   "install-autocomplete",
   350  		AutocompleteUninstall: "uninstall-autocomplete",
   351  	}
   352  
   353  	// Before we continue we'll check whether the requested command is
   354  	// actually known. If not, we might be able to suggest an alternative
   355  	// if it seems like the user made a typo.
   356  	// (This bypasses the built-in help handling in cli.CLI for the situation
   357  	// where a command isn't found, because it's likely more helpful to
   358  	// mention what specifically went wrong, rather than just printing out
   359  	// a big block of usage information.)
   360  
   361  	// Check if this is being run via shell auto-complete, which uses the
   362  	// binary name as the first argument and won't be listed as a subcommand.
   363  	autoComplete := os.Getenv("COMP_LINE") != ""
   364  
   365  	if cmd := cliRunner.Subcommand(); cmd != "" && !autoComplete {
   366  		// Due to the design of cli.CLI, this special error message only works
   367  		// for typos of top-level commands. For a subcommand typo, like
   368  		// "terraform state posh", cmd would be "state" here and thus would
   369  		// be considered to exist, and it would print out its own usage message.
   370  		if _, exists := Commands[cmd]; !exists {
   371  			suggestions := make([]string, 0, len(Commands))
   372  			for name := range Commands {
   373  				suggestions = append(suggestions, name)
   374  			}
   375  			suggestion := didyoumean.NameSuggestion(cmd, suggestions)
   376  			if suggestion != "" {
   377  				suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
   378  			}
   379  			fmt.Fprintf(os.Stderr, "Terraform has no command named %q.%s\n\nTo see all of Terraform's top-level commands, run:\n  terraform -help\n\n", cmd, suggestion)
   380  			return 1
   381  		}
   382  	}
   383  
   384  	exitCode, err := cliRunner.Run()
   385  	if err != nil {
   386  		Ui.Error(fmt.Sprintf("Error executing CLI: %s", err.Error()))
   387  		return 1
   388  	}
   389  
   390  	// if we are exiting with a non-zero code, check if it was caused by any
   391  	// plugins crashing
   392  	if exitCode != 0 {
   393  		for _, panicLog := range logging.PluginPanics() {
   394  			// we don't write this to Error, or else panicwrap will think this
   395  			// process panicked
   396  			Ui.Info(panicLog)
   397  		}
   398  	}
   399  
   400  	return exitCode
   401  }
   402  
   403  func mergeEnvArgs(envName string, cmd string, args []string) ([]string, error) {
   404  	v := os.Getenv(envName)
   405  	if v == "" {
   406  		return args, nil
   407  	}
   408  
   409  	log.Printf("[INFO] %s value: %q", envName, v)
   410  	extra, err := shellwords.Parse(v)
   411  	if err != nil {
   412  		return nil, fmt.Errorf(
   413  			"Error parsing extra CLI args from %s: %s",
   414  			envName, err)
   415  	}
   416  
   417  	// Find the command to look for in the args. If there is a space,
   418  	// we need to find the last part.
   419  	search := cmd
   420  	if idx := strings.LastIndex(search, " "); idx >= 0 {
   421  		search = cmd[idx+1:]
   422  	}
   423  
   424  	// Find the index to place the flags. We put them exactly
   425  	// after the first non-flag arg.
   426  	idx := -1
   427  	for i, v := range args {
   428  		if v == search {
   429  			idx = i
   430  			break
   431  		}
   432  	}
   433  
   434  	// idx points to the exact arg that isn't a flag. We increment
   435  	// by one so that all the copying below expects idx to be the
   436  	// insertion point.
   437  	idx++
   438  
   439  	// Copy the args
   440  	newArgs := make([]string, len(args)+len(extra))
   441  	copy(newArgs, args[:idx])
   442  	copy(newArgs[idx:], extra)
   443  	copy(newArgs[len(extra)+idx:], args[idx:])
   444  	return newArgs, nil
   445  }
   446  
   447  // parse information on reattaching to unmanaged providers out of a
   448  // JSON-encoded environment variable.
   449  func parseReattachProviders(in string) (map[addrs.Provider]*plugin.ReattachConfig, error) {
   450  	unmanagedProviders := map[addrs.Provider]*plugin.ReattachConfig{}
   451  	if in != "" {
   452  		type reattachConfig struct {
   453  			Protocol        string
   454  			ProtocolVersion int
   455  			Addr            struct {
   456  				Network string
   457  				String  string
   458  			}
   459  			Pid  int
   460  			Test bool
   461  		}
   462  		var m map[string]reattachConfig
   463  		err := json.Unmarshal([]byte(in), &m)
   464  		if err != nil {
   465  			return unmanagedProviders, fmt.Errorf("Invalid format for TF_REATTACH_PROVIDERS: %w", err)
   466  		}
   467  		for p, c := range m {
   468  			a, diags := addrs.ParseProviderSourceString(p)
   469  			if diags.HasErrors() {
   470  				return unmanagedProviders, fmt.Errorf("Error parsing %q as a provider address: %w", a, diags.Err())
   471  			}
   472  			var addr net.Addr
   473  			switch c.Addr.Network {
   474  			case "unix":
   475  				addr, err = net.ResolveUnixAddr("unix", c.Addr.String)
   476  				if err != nil {
   477  					return unmanagedProviders, fmt.Errorf("Invalid unix socket path %q for %q: %w", c.Addr.String, p, err)
   478  				}
   479  			case "tcp":
   480  				addr, err = net.ResolveTCPAddr("tcp", c.Addr.String)
   481  				if err != nil {
   482  					return unmanagedProviders, fmt.Errorf("Invalid TCP address %q for %q: %w", c.Addr.String, p, err)
   483  				}
   484  			default:
   485  				return unmanagedProviders, fmt.Errorf("Unknown address type %q for %q", c.Addr.Network, p)
   486  			}
   487  			unmanagedProviders[a] = &plugin.ReattachConfig{
   488  				Protocol:        plugin.Protocol(c.Protocol),
   489  				ProtocolVersion: c.ProtocolVersion,
   490  				Pid:             c.Pid,
   491  				Test:            c.Test,
   492  				Addr:            addr,
   493  			}
   494  		}
   495  	}
   496  	return unmanagedProviders, nil
   497  }
   498  
   499  func extractChdirOption(args []string) (string, []string, error) {
   500  	if len(args) == 0 {
   501  		return "", args, nil
   502  	}
   503  
   504  	const argName = "-chdir"
   505  	const argPrefix = argName + "="
   506  	var argValue string
   507  	var argPos int
   508  
   509  	for i, arg := range args {
   510  		if !strings.HasPrefix(arg, "-") {
   511  			// Because the chdir option is a subcommand-agnostic one, we require
   512  			// it to appear before any subcommand argument, so if we find a
   513  			// non-option before we find -chdir then we are finished.
   514  			break
   515  		}
   516  		if arg == argName || arg == argPrefix {
   517  			return "", args, fmt.Errorf("must include an equals sign followed by a directory path, like -chdir=example")
   518  		}
   519  		if strings.HasPrefix(arg, argPrefix) {
   520  			argPos = i
   521  			argValue = arg[len(argPrefix):]
   522  		}
   523  	}
   524  
   525  	// When we fall out here, we'll have populated argValue with a non-empty
   526  	// string if the -chdir=... option was present and valid, or left it
   527  	// empty if it wasn't present.
   528  	if argValue == "" {
   529  		return "", args, nil
   530  	}
   531  
   532  	// If we did find the option then we'll need to produce a new args that
   533  	// doesn't include it anymore.
   534  	if argPos == 0 {
   535  		// Easy case: we can just slice off the front
   536  		return argValue, args[1:], nil
   537  	}
   538  	// Otherwise we need to construct a new array and copy to it.
   539  	newArgs := make([]string, len(args)-1)
   540  	copy(newArgs, args[:argPos])
   541  	copy(newArgs[argPos:], args[argPos+1:])
   542  	return argValue, newArgs, nil
   543  }