github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/client/allocrunner/taskrunner/envoybootstrap_hook.go (about) 1 package taskrunner 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io/ioutil" 8 "os" 9 "os/exec" 10 "path/filepath" 11 "time" 12 13 "github.com/hashicorp/go-hclog" 14 "github.com/hashicorp/nomad/client/allocdir" 15 "github.com/hashicorp/nomad/client/allocrunner/interfaces" 16 agentconsul "github.com/hashicorp/nomad/command/agent/consul" 17 "github.com/hashicorp/nomad/helper" 18 "github.com/hashicorp/nomad/nomad/structs" 19 "github.com/hashicorp/nomad/nomad/structs/config" 20 "github.com/pkg/errors" 21 ) 22 23 const envoyBootstrapHookName = "envoy_bootstrap" 24 25 type envoyBootstrapConsulConfig struct { 26 HTTPAddr string // required 27 Auth string // optional, env CONSUL_HTTP_AUTH 28 SSL string // optional, env CONSUL_HTTP_SSL 29 VerifySSL string // optional, env CONSUL_HTTP_SSL_VERIFY 30 CAFile string // optional, arg -ca-file 31 CertFile string // optional, arg -client-cert 32 KeyFile string // optional, arg -client-key 33 // CAPath (dir) not supported by Nomad's config object 34 } 35 36 type envoyBootstrapHookConfig struct { 37 consul envoyBootstrapConsulConfig 38 alloc *structs.Allocation 39 logger hclog.Logger 40 } 41 42 func decodeTriState(b *bool) string { 43 switch { 44 case b == nil: 45 return "" 46 case *b: 47 return "true" 48 default: 49 return "false" 50 } 51 } 52 53 func newEnvoyBootstrapHookConfig(alloc *structs.Allocation, consul *config.ConsulConfig, logger hclog.Logger) *envoyBootstrapHookConfig { 54 return &envoyBootstrapHookConfig{ 55 alloc: alloc, 56 logger: logger, 57 consul: envoyBootstrapConsulConfig{ 58 HTTPAddr: consul.Addr, 59 Auth: consul.Auth, 60 SSL: decodeTriState(consul.EnableSSL), 61 VerifySSL: decodeTriState(consul.VerifySSL), 62 CAFile: consul.CAFile, 63 CertFile: consul.CertFile, 64 KeyFile: consul.KeyFile, 65 }, 66 } 67 } 68 69 const ( 70 envoyBaseAdminPort = 19000 71 envoyAdminBindEnvPrefix = "NOMAD_ENVOY_ADMIN_ADDR_" 72 ) 73 74 // envoyBootstrapHook writes the bootstrap config for the Connect Envoy proxy 75 // sidecar. 76 type envoyBootstrapHook struct { 77 // alloc is the allocation with the envoy task being bootstrapped. 78 alloc *structs.Allocation 79 80 // Bootstrapping Envoy requires talking directly to Consul to generate 81 // the bootstrap.json config. Runtime Envoy configuration is done via 82 // Consul's gRPC endpoint. There are many security parameters to configure 83 // before contacting Consul. 84 consulConfig envoyBootstrapConsulConfig 85 86 // logger is used to log things 87 logger hclog.Logger 88 } 89 90 func newEnvoyBootstrapHook(c *envoyBootstrapHookConfig) *envoyBootstrapHook { 91 return &envoyBootstrapHook{ 92 alloc: c.alloc, 93 consulConfig: c.consul, 94 logger: c.logger.Named(envoyBootstrapHookName), 95 } 96 } 97 98 func (envoyBootstrapHook) Name() string { 99 return envoyBootstrapHookName 100 } 101 102 func (h *envoyBootstrapHook) Prestart(ctx context.Context, req *interfaces.TaskPrestartRequest, resp *interfaces.TaskPrestartResponse) error { 103 if !req.Task.Kind.IsConnectProxy() { 104 // Not a Connect proxy sidecar 105 resp.Done = true 106 return nil 107 } 108 109 serviceName := req.Task.Kind.Value() 110 if serviceName == "" { 111 return errors.New("connect proxy sidecar does not specify service name") 112 } 113 114 tg := h.alloc.Job.LookupTaskGroup(h.alloc.TaskGroup) 115 116 var service *structs.Service 117 for _, s := range tg.Services { 118 if s.Name == serviceName { 119 service = s 120 break 121 } 122 } 123 124 if service == nil { 125 return errors.New("connect proxy sidecar task exists but no services configured with a sidecar") 126 } 127 128 h.logger.Debug("bootstrapping Connect proxy sidecar", "task", req.Task.Name, "service", serviceName) 129 130 //TODO Should connect directly to Consul if the sidecar is running on the host netns. 131 grpcAddr := "unix://" + allocdir.AllocGRPCSocket 132 133 // Envoy runs an administrative API on the loopback interface. If multiple sidecars 134 // are running, the bind addresses need to have unique ports. 135 // TODO: support running in host netns, using freeport to find available port 136 envoyAdminBind := buildEnvoyAdminBind(h.alloc, req.Task.Name) 137 resp.Env = map[string]string{ 138 helper.CleanEnvVar(envoyAdminBindEnvPrefix+serviceName, '_'): envoyAdminBind, 139 } 140 141 // Envoy bootstrap configuration may contain a Consul token, so write 142 // it to the secrets directory like Vault tokens. 143 bootstrapFilePath := filepath.Join(req.TaskDir.SecretsDir, "envoy_bootstrap.json") 144 145 id := agentconsul.MakeAllocServiceID(h.alloc.ID, "group-"+tg.Name, service) 146 147 h.logger.Debug("bootstrapping envoy", "sidecar_for", service.Name, "bootstrap_file", bootstrapFilePath, "sidecar_for_id", id, "grpc_addr", grpcAddr, "admin_bind", envoyAdminBind) 148 149 siToken, err := h.maybeLoadSIToken(req.Task.Name, req.TaskDir.SecretsDir) 150 if err != nil { 151 h.logger.Error("failed to generate envoy bootstrap config", "sidecar_for", service.Name) 152 return errors.Wrap(err, "failed to generate envoy bootstrap config") 153 } 154 h.logger.Debug("check for SI token for task", "task", req.Task.Name, "exists", siToken != "") 155 156 bootstrapBuilder := envoyBootstrapArgs{ 157 consulConfig: h.consulConfig, 158 sidecarFor: id, 159 grpcAddr: grpcAddr, 160 envoyAdminBind: envoyAdminBind, 161 siToken: siToken, 162 } 163 164 bootstrapArgs := bootstrapBuilder.args() 165 bootstrapEnv := bootstrapBuilder.env(os.Environ()) 166 167 // Since Consul services are registered asynchronously with this task 168 // hook running, retry a small number of times with backoff. 169 for tries := 3; ; tries-- { 170 171 cmd := exec.CommandContext(ctx, "consul", bootstrapArgs...) 172 cmd.Env = bootstrapEnv 173 174 // Redirect output to secrets/envoy_bootstrap.json 175 fd, err := os.Create(bootstrapFilePath) 176 if err != nil { 177 return fmt.Errorf("error creating secrets/envoy_bootstrap.json for envoy: %v", err) 178 } 179 cmd.Stdout = fd 180 181 buf := bytes.NewBuffer(nil) 182 cmd.Stderr = buf 183 184 // Generate bootstrap 185 err = cmd.Run() 186 187 // Close bootstrap.json 188 fd.Close() 189 190 if err == nil { 191 // Happy path! Bootstrap was created, exit. 192 break 193 } 194 195 // Check for error from command 196 if tries == 0 { 197 h.logger.Error("error creating bootstrap configuration for Connect proxy sidecar", "error", err, "stderr", buf.String()) 198 199 // Cleanup the bootstrap file. An errors here is not 200 // important as (a) we test to ensure the deletion 201 // occurs, and (b) the file will either be rewritten on 202 // retry or eventually garbage collected if the task 203 // fails. 204 os.Remove(bootstrapFilePath) 205 206 // ExitErrors are recoverable since they indicate the 207 // command was runnable but exited with a unsuccessful 208 // error code. 209 _, recoverable := err.(*exec.ExitError) 210 return structs.NewRecoverableError( 211 fmt.Errorf("error creating bootstrap configuration for Connect proxy sidecar: %v", err), 212 recoverable, 213 ) 214 } 215 216 // Sleep before retrying to give Consul services time to register 217 select { 218 case <-time.After(2 * time.Second): 219 case <-ctx.Done(): 220 // Killed before bootstrap, exit without setting Done 221 return nil 222 } 223 } 224 225 // Bootstrap written. Mark as done and move on. 226 resp.Done = true 227 return nil 228 } 229 230 func buildEnvoyAdminBind(alloc *structs.Allocation, taskName string) string { 231 port := envoyBaseAdminPort 232 for idx, task := range alloc.Job.LookupTaskGroup(alloc.TaskGroup).Tasks { 233 if task.Name == taskName { 234 port += idx 235 break 236 } 237 } 238 return fmt.Sprintf("localhost:%d", port) 239 } 240 241 func (h *envoyBootstrapHook) writeConfig(filename, config string) error { 242 if err := ioutil.WriteFile(filename, []byte(config), 0440); err != nil { 243 _ = os.Remove(filename) 244 return err 245 } 246 return nil 247 } 248 249 func (h *envoyBootstrapHook) execute(cmd *exec.Cmd) (string, error) { 250 var ( 251 stdout bytes.Buffer 252 stderr bytes.Buffer 253 ) 254 255 cmd.Stdout = &stdout 256 cmd.Stderr = &stderr 257 258 if err := cmd.Run(); err != nil { 259 _, recoverable := err.(*exec.ExitError) 260 // ExitErrors are recoverable since they indicate the 261 // command was runnable but exited with a unsuccessful 262 // error code. 263 return stderr.String(), structs.NewRecoverableError(err, recoverable) 264 } 265 return stdout.String(), nil 266 } 267 268 // envoyBootstrapArgs is used to accumulate CLI arguments that will be passed 269 // along to the exec invocation of consul which will then generate the bootstrap 270 // configuration file for envoy. 271 type envoyBootstrapArgs struct { 272 consulConfig envoyBootstrapConsulConfig 273 sidecarFor string 274 grpcAddr string 275 envoyAdminBind string 276 siToken string 277 } 278 279 // args returns the CLI arguments consul needs in the correct order, with the 280 // -token argument present or not present depending on whether it is set. 281 func (e envoyBootstrapArgs) args() []string { 282 arguments := []string{ 283 "connect", 284 "envoy", 285 "-grpc-addr", e.grpcAddr, 286 "-http-addr", e.consulConfig.HTTPAddr, 287 "-admin-bind", e.envoyAdminBind, 288 "-bootstrap", 289 "-sidecar-for", e.sidecarFor, 290 } 291 292 if v := e.siToken; v != "" { 293 arguments = append(arguments, "-token", v) 294 } 295 296 if v := e.consulConfig.CAFile; v != "" { 297 arguments = append(arguments, "-ca-file", v) 298 } 299 300 if v := e.consulConfig.CertFile; v != "" { 301 arguments = append(arguments, "-client-cert", v) 302 } 303 304 if v := e.consulConfig.KeyFile; v != "" { 305 arguments = append(arguments, "-client-key", v) 306 } 307 308 return arguments 309 } 310 311 // env creates the context of environment variables to be used when exec-ing 312 // the consul command for generating the envoy bootstrap config. It is expected 313 // the value of os.Environ() is passed in to be appended to. Because these are 314 // appended at the end of what will be passed into Cmd.Env, they will override 315 // any pre-existing values (i.e. what the Nomad agent was launched with). 316 // https://golang.org/pkg/os/exec/#Cmd 317 func (e envoyBootstrapArgs) env(env []string) []string { 318 if v := e.consulConfig.Auth; v != "" { 319 env = append(env, fmt.Sprintf("%s=%s", "CONSUL_HTTP_AUTH", v)) 320 } 321 if v := e.consulConfig.SSL; v != "" { 322 env = append(env, fmt.Sprintf("%s=%s", "CONSUL_HTTP_SSL", v)) 323 } 324 if v := e.consulConfig.VerifySSL; v != "" { 325 env = append(env, fmt.Sprintf("%s=%s", "CONSUL_HTTP_SSL_VERIFY", v)) 326 } 327 return env 328 } 329 330 // maybeLoadSIToken reads the SI token saved to disk in the secrets directory 331 // by the service identities prestart hook. This envoy bootstrap hook blocks 332 // until the sids hook completes, so if the SI token is required to exist (i.e. 333 // Consul ACLs are enabled), it will be in place by the time we try to read it. 334 func (h *envoyBootstrapHook) maybeLoadSIToken(task, dir string) (string, error) { 335 tokenPath := filepath.Join(dir, sidsTokenFile) 336 token, err := ioutil.ReadFile(tokenPath) 337 if err != nil { 338 if !os.IsNotExist(err) { 339 h.logger.Error("failed to load SI token", "task", task, "error", err) 340 return "", errors.Wrapf(err, "failed to load SI token for %s", task) 341 } 342 h.logger.Trace("no SI token to load", "task", task) 343 return "", nil // token file does not exist 344 } 345 h.logger.Trace("recovered pre-existing SI token", "task", task) 346 return string(token), nil 347 }