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