github.com/DerekStrickland/consul@v1.4.5/command/connect/envoy/envoy.go (about)

     1  package envoy
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"flag"
     7  	"fmt"
     8  	"html/template"
     9  	"net"
    10  	"os"
    11  	"os/exec"
    12  	"strconv"
    13  	"strings"
    14  
    15  	proxyAgent "github.com/hashicorp/consul/agent/proxyprocess"
    16  	"github.com/hashicorp/consul/agent/xds"
    17  	"github.com/hashicorp/consul/api"
    18  	proxyCmd "github.com/hashicorp/consul/command/connect/proxy"
    19  	"github.com/hashicorp/consul/command/flags"
    20  
    21  	"github.com/mitchellh/cli"
    22  )
    23  
    24  func New(ui cli.Ui) *cmd {
    25  	ui = &cli.PrefixedUi{
    26  		OutputPrefix: "==> ",
    27  		InfoPrefix:   "    ",
    28  		ErrorPrefix:  "==> ",
    29  		Ui:           ui,
    30  	}
    31  
    32  	c := &cmd{UI: ui}
    33  	c.init()
    34  	return c
    35  }
    36  
    37  type cmd struct {
    38  	UI     cli.Ui
    39  	flags  *flag.FlagSet
    40  	http   *flags.HTTPFlags
    41  	help   string
    42  	client *api.Client
    43  
    44  	// flags
    45  	proxyID    string
    46  	sidecarFor string
    47  	adminBind  string
    48  	envoyBin   string
    49  	bootstrap  bool
    50  	grpcAddr   string
    51  }
    52  
    53  func (c *cmd) init() {
    54  	c.flags = flag.NewFlagSet("", flag.ContinueOnError)
    55  
    56  	c.flags.StringVar(&c.proxyID, "proxy-id", "",
    57  		"The proxy's ID on the local agent.")
    58  
    59  	c.flags.StringVar(&c.sidecarFor, "sidecar-for", "",
    60  		"The ID of a service instance on the local agent that this proxy should "+
    61  			"become a sidecar for. It requires that the proxy service is registered "+
    62  			"with the agent as a connect-proxy with Proxy.DestinationServiceID set "+
    63  			"to this value. If more than one such proxy is registered it will fail.")
    64  
    65  	c.flags.StringVar(&c.envoyBin, "envoy-binary", "",
    66  		"The full path to the envoy binary to run. By default will just search "+
    67  			"$PATH. Ignored if -bootstrap is used.")
    68  
    69  	c.flags.StringVar(&c.adminBind, "admin-bind", "localhost:19000",
    70  		"The address:port to start envoy's admin server on. Envoy requires this "+
    71  			"but care must be taked to ensure it's not exposed to untrusted network "+
    72  			"as it has full control over the secrets and config of the proxy.")
    73  
    74  	c.flags.BoolVar(&c.bootstrap, "bootstrap", false,
    75  		"Generate the bootstrap.json but don't exec envoy")
    76  
    77  	c.flags.StringVar(&c.grpcAddr, "grpc-addr", "",
    78  		"Set the agent's gRPC address and port (in http(s)://host:port format). "+
    79  			"Alternatively, you can specify CONSUL_GRPC_ADDR in ENV.")
    80  
    81  	c.http = &flags.HTTPFlags{}
    82  	flags.Merge(c.flags, c.http.ClientFlags())
    83  	c.help = flags.Usage(help, c.flags)
    84  }
    85  
    86  func (c *cmd) Run(args []string) int {
    87  	if err := c.flags.Parse(args); err != nil {
    88  		return 1
    89  	}
    90  	passThroughArgs := c.flags.Args()
    91  
    92  	// Load the proxy ID and token from env vars if they're set
    93  	if c.proxyID == "" {
    94  		c.proxyID = os.Getenv(proxyAgent.EnvProxyID)
    95  	}
    96  	if c.sidecarFor == "" {
    97  		c.sidecarFor = os.Getenv(proxyAgent.EnvSidecarFor)
    98  	}
    99  	if c.grpcAddr == "" {
   100  		c.grpcAddr = os.Getenv(api.GRPCAddrEnvName)
   101  	}
   102  	if c.grpcAddr == "" {
   103  		// This is the dev mode default and recommended production setting if
   104  		// enabled.
   105  		c.grpcAddr = "localhost:8502"
   106  	}
   107  	if c.http.Token() == "" {
   108  		// Extra check needed since CONSUL_HTTP_TOKEN has not been consulted yet but
   109  		// calling SetToken with empty will force that to override the
   110  		if proxyToken := os.Getenv(proxyAgent.EnvProxyToken); proxyToken != "" {
   111  			c.http.SetToken(proxyToken)
   112  		}
   113  	}
   114  
   115  	// Setup Consul client
   116  	client, err := c.http.APIClient()
   117  	if err != nil {
   118  		c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
   119  		return 1
   120  	}
   121  	c.client = client
   122  
   123  	// See if we need to lookup proxyID
   124  	if c.proxyID == "" && c.sidecarFor != "" {
   125  		proxyID, err := c.lookupProxyIDForSidecar()
   126  		if err != nil {
   127  			c.UI.Error(err.Error())
   128  			return 1
   129  		}
   130  		c.proxyID = proxyID
   131  	}
   132  	if c.proxyID == "" {
   133  		c.UI.Error("No proxy ID specified. One of -proxy-id or -sidecar-for is " +
   134  			"required")
   135  		return 1
   136  	}
   137  
   138  	// Generate config
   139  	bootstrapJson, err := c.generateConfig()
   140  	if err != nil {
   141  		c.UI.Error(err.Error())
   142  		return 1
   143  	}
   144  
   145  	if c.bootstrap {
   146  		// Just output it and we are done
   147  		os.Stdout.Write(bootstrapJson)
   148  		return 0
   149  	}
   150  
   151  	// Find Envoy binary
   152  	binary, err := c.findBinary()
   153  	if err != nil {
   154  		c.UI.Error("Couldn't find envoy binary: " + err.Error())
   155  		return 1
   156  	}
   157  
   158  	err = execEnvoy(binary, nil, passThroughArgs, bootstrapJson)
   159  	if err == errUnsupportedOS {
   160  		c.UI.Error("Directly running Envoy is only supported on linux and macOS " +
   161  			"since envoy itself doesn't build on other platforms currently.")
   162  		c.UI.Error("Use the -bootstrap option to generate the JSON to use when running envoy " +
   163  			"on a supported OS or via a container or VM.")
   164  		return 1
   165  	} else if err != nil {
   166  		c.UI.Error(err.Error())
   167  		return 1
   168  	}
   169  
   170  	return 0
   171  }
   172  
   173  var errUnsupportedOS = errors.New("envoy: not implemented on this operating system")
   174  
   175  func (c *cmd) findBinary() (string, error) {
   176  	if c.envoyBin != "" {
   177  		return c.envoyBin, nil
   178  	}
   179  	return exec.LookPath("envoy")
   180  }
   181  
   182  func (c *cmd) templateArgs() (*templateArgs, error) {
   183  	httpCfg := api.DefaultConfig()
   184  	c.http.MergeOntoConfig(httpCfg)
   185  
   186  	// Decide on TLS if the scheme is provided and indicates it, if the HTTP env
   187  	// suggests TLS is supported explicitly (CONSUL_HTTP_SSL) or implicitly
   188  	// (CONSUL_HTTP_ADDR) is https://
   189  	useTLS := false
   190  	if strings.HasPrefix(strings.ToLower(c.grpcAddr), "https://") {
   191  		useTLS = true
   192  	} else if useSSLEnv := os.Getenv(api.HTTPSSLEnvName); useSSLEnv != "" {
   193  		if enabled, err := strconv.ParseBool(useSSLEnv); err != nil {
   194  			useTLS = enabled
   195  		}
   196  	} else if strings.HasPrefix(strings.ToLower(httpCfg.Address), "https://") {
   197  		useTLS = true
   198  	}
   199  
   200  	// We want to allow grpcAddr set as host:port with no scheme but if the host
   201  	// is an IP this will fail to parse as a URL with "parse 127.0.0.1:8500: first
   202  	// path segment in URL cannot contain colon". On the other hand we also
   203  	// support both http(s)://host:port and unix:///path/to/file.
   204  	addrPort := strings.TrimPrefix(c.grpcAddr, "http://")
   205  	addrPort = strings.TrimPrefix(c.grpcAddr, "https://")
   206  
   207  	agentAddr, agentPort, err := net.SplitHostPort(addrPort)
   208  	if err != nil {
   209  		return nil, fmt.Errorf("Invalid Consul HTTP address: %s", err)
   210  	}
   211  	if agentAddr == "" {
   212  		agentAddr = "127.0.0.1"
   213  	}
   214  
   215  	// We use STATIC for agent which means we need to resolve DNS names like
   216  	// `localhost` ourselves. We could use STRICT_DNS or LOGICAL_DNS with envoy
   217  	// but Envoy resolves `localhost` differently to go on macOS at least which
   218  	// causes paper cuts like default dev agent (which binds specifically to
   219  	// 127.0.0.1) isn't reachable since Envoy resolves localhost to `[::]` and
   220  	// can't connect.
   221  	agentIP, err := net.ResolveIPAddr("ip", agentAddr)
   222  	if err != nil {
   223  		return nil, fmt.Errorf("Failed to resolve agent address: %s", err)
   224  	}
   225  
   226  	adminAddr, adminPort, err := net.SplitHostPort(c.adminBind)
   227  	if err != nil {
   228  		return nil, fmt.Errorf("Invalid Consul HTTP address: %s", err)
   229  	}
   230  
   231  	// Envoy requires IP addresses to bind too when using static so resolve DNS or
   232  	// localhost here.
   233  	adminBindIP, err := net.ResolveIPAddr("ip", adminAddr)
   234  	if err != nil {
   235  		return nil, fmt.Errorf("Failed to resolve admin bind address: %s", err)
   236  	}
   237  
   238  	return &templateArgs{
   239  		ProxyCluster:          c.proxyID,
   240  		ProxyID:               c.proxyID,
   241  		AgentAddress:          agentIP.String(),
   242  		AgentPort:             agentPort,
   243  		AgentTLS:              useTLS,
   244  		AgentCAFile:           httpCfg.TLSConfig.CAFile,
   245  		AdminBindAddress:      adminBindIP.String(),
   246  		AdminBindPort:         adminPort,
   247  		Token:                 httpCfg.Token,
   248  		LocalAgentClusterName: xds.LocalAgentClusterName,
   249  	}, nil
   250  }
   251  
   252  func (c *cmd) generateConfig() ([]byte, error) {
   253  	args, err := c.templateArgs()
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  	var t = template.Must(template.New("bootstrap").Parse(bootstrapTemplate))
   258  	var buf bytes.Buffer
   259  	err = t.Execute(&buf, args)
   260  	if err != nil {
   261  		return nil, err
   262  	}
   263  	return buf.Bytes(), nil
   264  }
   265  
   266  func (c *cmd) lookupProxyIDForSidecar() (string, error) {
   267  	return proxyCmd.LookupProxyIDForSidecar(c.client, c.sidecarFor)
   268  }
   269  
   270  func (c *cmd) Synopsis() string {
   271  	return synopsis
   272  }
   273  
   274  func (c *cmd) Help() string {
   275  	return c.help
   276  }
   277  
   278  const synopsis = "Runs or Configures Envoy as a Connect proxy"
   279  const help = `
   280  Usage: consul connect envoy [options]
   281  
   282    Generates the bootstrap configuration needed to start an Envoy proxy instance
   283    for use as a Connect sidecar for a particular service instance. By default it
   284    will generate the config and then exec Envoy directly until it exits normally.
   285  
   286    It will search $PATH for the envoy binary but this can be overridden with
   287    -envoy-binary.
   288  
   289    It can instead only generate the bootstrap.json based on the current ENV and
   290    arguments using -bootstrap.
   291  
   292    The proxy requires service:write permissions for the service it represents.
   293    The token may be passed via the CLI or the CONSUL_TOKEN environment
   294    variable.
   295  
   296    The example below shows how to start a local proxy as a sidecar to a "web"
   297    service instance. It assumes that the proxy was already registered with it's
   298    Config for example via a sidecar_service block.
   299  
   300      $ consul connect envoy -sidecar-for web
   301  
   302  `