github.com/kevinklinger/open_terraform@v1.3.6/main.go (about)

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