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 `