2023-04-10 15:36:59 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
2019-08-12 22:41:39 +00:00
|
|
|
package taskrunner
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2022-02-01 23:55:36 +00:00
|
|
|
"encoding/json"
|
2022-04-02 00:24:02 +00:00
|
|
|
"errors"
|
2019-08-12 22:41:39 +00:00
|
|
|
"fmt"
|
2022-02-01 23:55:36 +00:00
|
|
|
"io"
|
2021-07-12 14:28:16 +00:00
|
|
|
"net"
|
2019-08-12 22:41:39 +00:00
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"path/filepath"
|
2021-07-12 14:28:16 +00:00
|
|
|
"strconv"
|
2022-02-01 23:55:36 +00:00
|
|
|
"strings"
|
2019-08-12 22:41:39 +00:00
|
|
|
"time"
|
|
|
|
|
2019-12-18 16:23:16 +00:00
|
|
|
"github.com/hashicorp/go-hclog"
|
2019-08-12 22:41:39 +00:00
|
|
|
"github.com/hashicorp/nomad/client/allocdir"
|
2020-09-04 17:50:11 +00:00
|
|
|
ifs "github.com/hashicorp/nomad/client/allocrunner/interfaces"
|
2022-03-15 08:38:30 +00:00
|
|
|
"github.com/hashicorp/nomad/client/serviceregistration"
|
2021-03-01 08:39:14 +00:00
|
|
|
"github.com/hashicorp/nomad/client/taskenv"
|
2020-01-07 02:53:45 +00:00
|
|
|
"github.com/hashicorp/nomad/helper"
|
2019-08-12 22:41:39 +00:00
|
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
2020-04-02 16:30:38 +00:00
|
|
|
"github.com/hashicorp/nomad/nomad/structs/config"
|
2022-01-31 20:49:05 +00:00
|
|
|
"oss.indeed.com/go/libtime/decay"
|
2019-08-12 22:41:39 +00:00
|
|
|
)
|
|
|
|
|
2019-12-18 16:23:16 +00:00
|
|
|
const envoyBootstrapHookName = "envoy_bootstrap"
|
|
|
|
|
2021-04-26 19:24:06 +00:00
|
|
|
const (
|
|
|
|
// envoyBootstrapWaitTime is the amount of time this hook should wait on Consul
|
|
|
|
// objects to be created before giving up.
|
|
|
|
envoyBootstrapWaitTime = 60 * time.Second
|
|
|
|
|
|
|
|
// envoyBootstrapInitialGap is the initial amount of time the envoy bootstrap
|
|
|
|
// retry loop will wait, exponentially increasing each iteration, not including
|
|
|
|
// jitter.
|
|
|
|
envoyBoostrapInitialGap = 1 * time.Second
|
|
|
|
|
|
|
|
// envoyBootstrapMaxJitter is the maximum amount of jitter applied to the
|
|
|
|
// wait gap each iteration of the envoy bootstrap retry loop.
|
|
|
|
envoyBootstrapMaxJitter = 500 * time.Millisecond
|
|
|
|
)
|
|
|
|
|
2023-02-02 20:20:26 +00:00
|
|
|
var (
|
|
|
|
errEnvoyBootstrapError = errors.New("error creating bootstrap configuration for Connect proxy sidecar")
|
|
|
|
)
|
|
|
|
|
2020-05-13 20:15:55 +00:00
|
|
|
type consulTransportConfig struct {
|
2023-01-11 15:34:28 +00:00
|
|
|
HTTPAddr string // required
|
|
|
|
Auth string // optional, env CONSUL_HTTP_AUTH
|
|
|
|
SSL string // optional, env CONSUL_HTTP_SSL
|
|
|
|
VerifySSL string // optional, env CONSUL_HTTP_SSL_VERIFY
|
|
|
|
GRPCCAFile string // optional, arg -grpc-ca-file
|
|
|
|
CAFile string // optional, arg -ca-file
|
|
|
|
CertFile string // optional, arg -client-cert
|
|
|
|
KeyFile string // optional, arg -client-key
|
|
|
|
Namespace string // optional, only consul Enterprise, env CONSUL_NAMESPACE
|
2020-04-02 16:30:38 +00:00
|
|
|
// CAPath (dir) not supported by Nomad's config object
|
|
|
|
}
|
|
|
|
|
2023-01-11 15:34:28 +00:00
|
|
|
func newConsulTransportConfig(cc *config.ConsulConfig) consulTransportConfig {
|
2020-05-13 20:15:55 +00:00
|
|
|
return consulTransportConfig{
|
2023-01-11 15:34:28 +00:00
|
|
|
HTTPAddr: cc.Addr,
|
|
|
|
Auth: cc.Auth,
|
|
|
|
SSL: decodeTriState(cc.EnableSSL),
|
|
|
|
VerifySSL: decodeTriState(cc.VerifySSL),
|
|
|
|
GRPCCAFile: cc.GRPCCAFile,
|
|
|
|
CAFile: cc.CAFile,
|
|
|
|
CertFile: cc.CertFile,
|
|
|
|
KeyFile: cc.KeyFile,
|
|
|
|
Namespace: cc.Namespace,
|
2020-05-13 20:15:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-18 16:23:16 +00:00
|
|
|
type envoyBootstrapHookConfig struct {
|
2021-03-16 18:22:21 +00:00
|
|
|
alloc *structs.Allocation
|
|
|
|
consul consulTransportConfig
|
|
|
|
consulNamespace string
|
|
|
|
logger hclog.Logger
|
2020-04-02 16:30:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func decodeTriState(b *bool) string {
|
|
|
|
switch {
|
|
|
|
case b == nil:
|
|
|
|
return ""
|
|
|
|
case *b:
|
|
|
|
return "true"
|
|
|
|
default:
|
|
|
|
return "false"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-16 18:22:21 +00:00
|
|
|
func newEnvoyBootstrapHookConfig(alloc *structs.Allocation, consul *config.ConsulConfig, consulNamespace string, logger hclog.Logger) *envoyBootstrapHookConfig {
|
2020-04-02 16:30:38 +00:00
|
|
|
return &envoyBootstrapHookConfig{
|
2021-03-16 18:22:21 +00:00
|
|
|
alloc: alloc,
|
|
|
|
consul: newConsulTransportConfig(consul),
|
|
|
|
consulNamespace: consulNamespace,
|
|
|
|
logger: logger,
|
2020-04-02 16:30:38 +00:00
|
|
|
}
|
2019-12-18 16:23:16 +00:00
|
|
|
}
|
2019-08-12 22:41:39 +00:00
|
|
|
|
2020-01-07 02:53:45 +00:00
|
|
|
const (
|
2021-07-09 18:44:02 +00:00
|
|
|
envoyBaseAdminPort = 19000 // Consul default (bridge only)
|
|
|
|
envoyBaseReadyPort = 19100 // Consul default (bridge only)
|
2020-01-07 02:53:45 +00:00
|
|
|
envoyAdminBindEnvPrefix = "NOMAD_ENVOY_ADMIN_ADDR_"
|
2021-07-09 18:44:02 +00:00
|
|
|
envoyReadyBindEnvPrefix = "NOMAD_ENVOY_READY_ADDR_"
|
2020-01-07 02:53:45 +00:00
|
|
|
)
|
2019-12-06 06:49:01 +00:00
|
|
|
|
2020-07-28 20:12:08 +00:00
|
|
|
const (
|
|
|
|
grpcConsulVariable = "CONSUL_GRPC_ADDR"
|
|
|
|
grpcDefaultAddress = "127.0.0.1:8502"
|
|
|
|
)
|
|
|
|
|
2019-08-12 22:41:39 +00:00
|
|
|
// envoyBootstrapHook writes the bootstrap config for the Connect Envoy proxy
|
|
|
|
// sidecar.
|
|
|
|
type envoyBootstrapHook struct {
|
2019-12-18 16:23:16 +00:00
|
|
|
// alloc is the allocation with the envoy task being bootstrapped.
|
2019-08-12 22:41:39 +00:00
|
|
|
alloc *structs.Allocation
|
|
|
|
|
|
|
|
// Bootstrapping Envoy requires talking directly to Consul to generate
|
|
|
|
// the bootstrap.json config. Runtime Envoy configuration is done via
|
2020-04-02 16:30:38 +00:00
|
|
|
// Consul's gRPC endpoint. There are many security parameters to configure
|
|
|
|
// before contacting Consul.
|
2020-05-13 20:15:55 +00:00
|
|
|
consulConfig consulTransportConfig
|
2019-08-12 22:41:39 +00:00
|
|
|
|
2021-03-16 18:22:21 +00:00
|
|
|
// consulNamespace is the Consul namespace as set by in the job
|
|
|
|
consulNamespace string
|
|
|
|
|
2021-04-26 19:24:06 +00:00
|
|
|
// envoyBootstrapWaitTime is the total amount of time hook will wait for Consul
|
|
|
|
envoyBootstrapWaitTime time.Duration
|
|
|
|
|
2023-01-11 15:34:28 +00:00
|
|
|
// envoyBootstrapInitialGap is the initial wait gap when retrying
|
2021-04-26 19:24:06 +00:00
|
|
|
envoyBoostrapInitialGap time.Duration
|
|
|
|
|
|
|
|
// envoyBootstrapMaxJitter is the maximum amount of jitter applied to retries
|
|
|
|
envoyBootstrapMaxJitter time.Duration
|
|
|
|
|
|
|
|
// envoyBootstrapExpSleep controls exponential waiting
|
|
|
|
envoyBootstrapExpSleep func(time.Duration)
|
|
|
|
|
2019-12-18 16:23:16 +00:00
|
|
|
// logger is used to log things
|
|
|
|
logger hclog.Logger
|
2019-08-12 22:41:39 +00:00
|
|
|
}
|
|
|
|
|
2019-12-18 16:23:16 +00:00
|
|
|
func newEnvoyBootstrapHook(c *envoyBootstrapHookConfig) *envoyBootstrapHook {
|
|
|
|
return &envoyBootstrapHook{
|
2021-04-26 19:24:06 +00:00
|
|
|
alloc: c.alloc,
|
|
|
|
consulConfig: c.consul,
|
|
|
|
consulNamespace: c.consulNamespace,
|
|
|
|
envoyBootstrapWaitTime: envoyBootstrapWaitTime,
|
|
|
|
envoyBoostrapInitialGap: envoyBoostrapInitialGap,
|
|
|
|
envoyBootstrapMaxJitter: envoyBootstrapMaxJitter,
|
|
|
|
envoyBootstrapExpSleep: time.Sleep,
|
|
|
|
logger: c.logger.Named(envoyBootstrapHookName),
|
2019-08-12 22:41:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-16 18:22:21 +00:00
|
|
|
// getConsulNamespace will resolve the Consul namespace, choosing between
|
2022-08-16 14:06:30 +00:00
|
|
|
// - agent config (low precedence)
|
|
|
|
// - task group config (high precedence)
|
2021-03-16 18:22:21 +00:00
|
|
|
func (h *envoyBootstrapHook) getConsulNamespace() string {
|
|
|
|
var namespace string
|
|
|
|
if h.consulConfig.Namespace != "" {
|
|
|
|
namespace = h.consulConfig.Namespace
|
|
|
|
}
|
|
|
|
if h.consulNamespace != "" {
|
|
|
|
namespace = h.consulNamespace
|
|
|
|
}
|
|
|
|
return namespace
|
|
|
|
}
|
|
|
|
|
2019-08-12 22:41:39 +00:00
|
|
|
func (envoyBootstrapHook) Name() string {
|
2019-12-18 16:23:16 +00:00
|
|
|
return envoyBootstrapHookName
|
2019-08-12 22:41:39 +00:00
|
|
|
}
|
|
|
|
|
2020-12-15 20:38:33 +00:00
|
|
|
func isConnectKind(kind string) bool {
|
2021-04-12 19:10:10 +00:00
|
|
|
switch kind {
|
|
|
|
case structs.ConnectProxyPrefix:
|
|
|
|
return true
|
|
|
|
case structs.ConnectIngressPrefix:
|
|
|
|
return true
|
|
|
|
case structs.ConnectTerminatingPrefix:
|
|
|
|
return true
|
|
|
|
case structs.ConnectMeshPrefix:
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
2020-12-15 20:38:33 +00:00
|
|
|
}
|
|
|
|
|
2020-07-28 20:12:08 +00:00
|
|
|
func (_ *envoyBootstrapHook) extractNameAndKind(kind structs.TaskKind) (string, string, error) {
|
|
|
|
serviceName := kind.Value()
|
2020-12-15 20:38:33 +00:00
|
|
|
serviceKind := kind.Name()
|
2020-07-28 20:12:08 +00:00
|
|
|
|
2020-12-15 20:38:33 +00:00
|
|
|
if !isConnectKind(serviceKind) {
|
2020-07-28 20:12:08 +00:00
|
|
|
return "", "", errors.New("envoy must be used as connect sidecar or gateway")
|
2019-08-12 22:41:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if serviceName == "" {
|
2020-07-28 20:12:08 +00:00
|
|
|
return "", "", errors.New("envoy must be configured with a service name")
|
2019-08-12 22:41:39 +00:00
|
|
|
}
|
|
|
|
|
2020-07-28 20:12:08 +00:00
|
|
|
return serviceKind, serviceName, nil
|
|
|
|
}
|
|
|
|
|
2021-04-12 19:10:10 +00:00
|
|
|
func (h *envoyBootstrapHook) lookupService(svcKind, svcName string, taskEnv *taskenv.TaskEnv) (*structs.Service, error) {
|
2019-08-12 22:41:39 +00:00
|
|
|
tg := h.alloc.Job.LookupTaskGroup(h.alloc.TaskGroup)
|
2021-03-01 08:39:14 +00:00
|
|
|
interpolatedServices := taskenv.InterpolateServices(taskEnv, tg.Services)
|
2019-08-12 22:41:39 +00:00
|
|
|
|
|
|
|
var service *structs.Service
|
2021-03-01 08:39:14 +00:00
|
|
|
for _, s := range interpolatedServices {
|
2020-07-28 20:12:08 +00:00
|
|
|
if s.Name == svcName {
|
2019-08-12 22:41:39 +00:00
|
|
|
service = s
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if service == nil {
|
2020-07-28 20:12:08 +00:00
|
|
|
if svcKind == structs.ConnectProxyPrefix {
|
|
|
|
return nil, errors.New("connect proxy sidecar task exists but no services configured with a sidecar")
|
|
|
|
} else {
|
|
|
|
return nil, errors.New("connect gateway task exists but no service associated")
|
|
|
|
}
|
2019-08-12 22:41:39 +00:00
|
|
|
}
|
|
|
|
|
2020-07-28 20:12:08 +00:00
|
|
|
return service, nil
|
|
|
|
}
|
2019-08-12 22:41:39 +00:00
|
|
|
|
2020-07-28 20:12:08 +00:00
|
|
|
// Prestart creates an envoy bootstrap config file.
|
|
|
|
//
|
|
|
|
// Must be aware of both launching envoy as a sidecar proxy, as well as a connect gateway.
|
2020-09-04 17:50:11 +00:00
|
|
|
func (h *envoyBootstrapHook) Prestart(ctx context.Context, req *ifs.TaskPrestartRequest, resp *ifs.TaskPrestartResponse) error {
|
2020-07-28 20:12:08 +00:00
|
|
|
if !req.Task.Kind.IsConnectProxy() && !req.Task.Kind.IsAnyConnectGateway() {
|
|
|
|
// Not a Connect proxy sidecar
|
|
|
|
resp.Done = true
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
serviceKind, serviceName, err := h.extractNameAndKind(req.Task.Kind)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-04-12 19:10:10 +00:00
|
|
|
service, err := h.lookupService(serviceKind, serviceName, req.TaskEnv)
|
2020-07-28 20:12:08 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
grpcAddr := h.grpcAddress(req.TaskEnv.EnvMap)
|
2019-08-12 22:41:39 +00:00
|
|
|
|
2020-07-28 20:12:08 +00:00
|
|
|
h.logger.Debug("bootstrapping Consul "+serviceKind, "task", req.Task.Name, "service", serviceName)
|
|
|
|
|
2021-07-09 18:44:02 +00:00
|
|
|
// Envoy runs an administrative listener. There is no way to turn this feature off.
|
2020-07-28 20:12:08 +00:00
|
|
|
// https://github.com/envoyproxy/envoy/issues/1297
|
2021-03-01 08:39:14 +00:00
|
|
|
envoyAdminBind := buildEnvoyAdminBind(h.alloc, serviceName, req.Task.Name, req.TaskEnv)
|
2021-07-09 18:44:02 +00:00
|
|
|
|
|
|
|
// Consul configures a ready listener. There is no way to turn this feature off.
|
|
|
|
envoyReadyBind := buildEnvoyReadyBind(h.alloc, serviceName, req.Task.Name, req.TaskEnv)
|
|
|
|
|
|
|
|
// Set runtime environment variables for the envoy admin and ready listeners.
|
2020-01-08 18:41:38 +00:00
|
|
|
resp.Env = map[string]string{
|
|
|
|
helper.CleanEnvVar(envoyAdminBindEnvPrefix+serviceName, '_'): envoyAdminBind,
|
2021-07-09 18:44:02 +00:00
|
|
|
helper.CleanEnvVar(envoyReadyBindEnvPrefix+serviceName, '_'): envoyReadyBind,
|
2020-01-08 18:41:38 +00:00
|
|
|
}
|
2019-12-06 06:49:01 +00:00
|
|
|
|
2019-08-12 22:41:39 +00:00
|
|
|
// Envoy bootstrap configuration may contain a Consul token, so write
|
|
|
|
// it to the secrets directory like Vault tokens.
|
2019-12-18 16:23:16 +00:00
|
|
|
bootstrapFilePath := filepath.Join(req.TaskDir.SecretsDir, "envoy_bootstrap.json")
|
2019-08-12 22:41:39 +00:00
|
|
|
|
2022-02-01 23:55:36 +00:00
|
|
|
// Write everything related to the command to enable debugging
|
|
|
|
bootstrapStderrPath := filepath.Join(req.TaskDir.LogDir, "envoy_bootstrap.stderr.0")
|
|
|
|
bootstrapEnvPath := filepath.Join(req.TaskDir.SecretsDir, ".envoy_bootstrap.env")
|
|
|
|
bootstrapCmdPath := filepath.Join(req.TaskDir.SecretsDir, ".envoy_bootstrap.cmd")
|
|
|
|
|
2019-12-18 16:23:16 +00:00
|
|
|
siToken, err := h.maybeLoadSIToken(req.Task.Name, req.TaskDir.SecretsDir)
|
|
|
|
if err != nil {
|
|
|
|
h.logger.Error("failed to generate envoy bootstrap config", "sidecar_for", service.Name)
|
2022-04-02 00:24:02 +00:00
|
|
|
return fmt.Errorf("failed to generate envoy bootstrap config: %w", err)
|
2019-12-18 16:23:16 +00:00
|
|
|
}
|
|
|
|
h.logger.Debug("check for SI token for task", "task", req.Task.Name, "exists", siToken != "")
|
|
|
|
|
2021-07-09 18:44:02 +00:00
|
|
|
bootstrap := h.newEnvoyBootstrapArgs(h.alloc.TaskGroup, service, grpcAddr, envoyAdminBind, envoyReadyBind, siToken, bootstrapFilePath)
|
2022-02-01 23:55:36 +00:00
|
|
|
|
|
|
|
// Create command line arguments
|
2020-07-28 20:12:08 +00:00
|
|
|
bootstrapArgs := bootstrap.args()
|
2022-02-01 23:55:36 +00:00
|
|
|
|
|
|
|
// Write args to file for debugging
|
|
|
|
argsFile, err := os.Create(bootstrapCmdPath)
|
|
|
|
if err != nil {
|
2022-04-02 00:24:02 +00:00
|
|
|
return fmt.Errorf("failed to write bootstrap command line: %w", err)
|
2022-02-01 23:55:36 +00:00
|
|
|
}
|
|
|
|
defer argsFile.Close()
|
|
|
|
if _, err := io.WriteString(argsFile, strings.Join(bootstrapArgs, " ")+"\n"); err != nil {
|
2022-04-02 00:24:02 +00:00
|
|
|
return fmt.Errorf("failed to encode bootstrap command line: %w", err)
|
2022-02-01 23:55:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Create environment
|
2020-07-28 20:12:08 +00:00
|
|
|
bootstrapEnv := bootstrap.env(os.Environ())
|
2022-09-22 18:18:18 +00:00
|
|
|
// append nomad environment variables to the bootstrap environment
|
|
|
|
bootstrapEnv = append(bootstrapEnv, h.groupEnv()...)
|
2019-12-18 16:23:16 +00:00
|
|
|
|
2022-02-01 23:55:36 +00:00
|
|
|
// Write env to file for debugging
|
|
|
|
envFile, err := os.Create(bootstrapEnvPath)
|
|
|
|
if err != nil {
|
2022-04-02 00:24:02 +00:00
|
|
|
return fmt.Errorf("failed to write bootstrap environment: %w", err)
|
2022-02-01 23:55:36 +00:00
|
|
|
}
|
|
|
|
defer envFile.Close()
|
|
|
|
envEnc := json.NewEncoder(envFile)
|
|
|
|
envEnc.SetIndent("", " ")
|
|
|
|
if err := envEnc.Encode(bootstrapEnv); err != nil {
|
2022-04-02 00:24:02 +00:00
|
|
|
return fmt.Errorf("failed to encode bootstrap environment: %w", err)
|
2022-02-01 23:55:36 +00:00
|
|
|
}
|
|
|
|
|
2021-04-26 19:24:06 +00:00
|
|
|
// keep track of latest error returned from exec-ing consul envoy bootstrap
|
|
|
|
var cmdErr error
|
|
|
|
|
2019-08-12 22:41:39 +00:00
|
|
|
// Since Consul services are registered asynchronously with this task
|
2021-04-26 19:24:06 +00:00
|
|
|
// hook running, retry until timeout or success.
|
2022-02-01 23:55:36 +00:00
|
|
|
backoffOpts := decay.BackoffOptions{
|
|
|
|
MaxSleepTime: h.envoyBootstrapWaitTime,
|
|
|
|
InitialGapSize: h.envoyBoostrapInitialGap,
|
|
|
|
MaxJitterSize: h.envoyBootstrapMaxJitter,
|
|
|
|
}
|
|
|
|
backoffErr := decay.Backoff(func() (bool, error) {
|
2021-04-26 19:24:06 +00:00
|
|
|
// If hook is killed, just stop.
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return false, nil
|
|
|
|
default:
|
|
|
|
}
|
2019-12-18 16:23:16 +00:00
|
|
|
|
2021-04-26 19:24:06 +00:00
|
|
|
// Prepare bootstrap command to run.
|
2019-12-18 16:23:16 +00:00
|
|
|
cmd := exec.CommandContext(ctx, "consul", bootstrapArgs...)
|
2020-04-02 16:30:38 +00:00
|
|
|
cmd.Env = bootstrapEnv
|
2019-08-12 22:41:39 +00:00
|
|
|
|
2021-04-26 19:24:06 +00:00
|
|
|
// Redirect stdout to secrets/envoy_bootstrap.json.
|
2022-02-01 23:55:36 +00:00
|
|
|
stdout, fileErr := os.Create(bootstrapFilePath)
|
2021-04-26 19:24:06 +00:00
|
|
|
if fileErr != nil {
|
|
|
|
return false, fmt.Errorf("failed to create secrets/envoy_bootstrap.json for envoy: %w", fileErr)
|
2019-08-12 22:41:39 +00:00
|
|
|
}
|
2022-02-01 23:55:36 +00:00
|
|
|
defer stdout.Close()
|
|
|
|
cmd.Stdout = stdout
|
2019-08-12 22:41:39 +00:00
|
|
|
|
2022-02-01 23:55:36 +00:00
|
|
|
// Redirect stderr into another file for later debugging.
|
|
|
|
stderr, fileErr := os.OpenFile(bootstrapStderrPath, os.O_RDWR|os.O_CREATE, 0644)
|
|
|
|
if fileErr != nil {
|
|
|
|
return false, fmt.Errorf("failed to create alloc/logs/envoy_bootstrap.stderr.0 for envoy: %w", fileErr)
|
|
|
|
}
|
|
|
|
defer stderr.Close()
|
|
|
|
cmd.Stderr = stderr
|
2019-08-12 22:41:39 +00:00
|
|
|
|
|
|
|
// Generate bootstrap
|
2021-04-26 19:24:06 +00:00
|
|
|
cmdErr = cmd.Run()
|
2019-08-12 22:41:39 +00:00
|
|
|
|
2021-04-26 19:24:06 +00:00
|
|
|
// Command succeeded, exit.
|
|
|
|
if cmdErr == nil {
|
|
|
|
// Bootstrap written. Mark as done and move on.
|
|
|
|
resp.Done = true
|
|
|
|
return false, nil
|
2019-08-12 22:41:39 +00:00
|
|
|
}
|
|
|
|
|
2021-04-26 19:24:06 +00:00
|
|
|
// Command failed, prepare for retry
|
|
|
|
//
|
|
|
|
// Cleanup the bootstrap file. An errors here is not
|
|
|
|
// important as (a) we test to ensure the deletion
|
|
|
|
// occurs, and (b) the file will either be rewritten on
|
|
|
|
// retry or eventually garbage collected if the task
|
|
|
|
// fails.
|
|
|
|
_ = os.Remove(bootstrapFilePath)
|
|
|
|
|
|
|
|
return true, cmdErr
|
2022-02-01 23:55:36 +00:00
|
|
|
}, backoffOpts)
|
|
|
|
|
|
|
|
if backoffErr != nil {
|
2021-04-26 19:24:06 +00:00
|
|
|
// Wrap the last error from Consul and set that as our status.
|
|
|
|
_, recoverable := cmdErr.(*exec.ExitError)
|
|
|
|
return structs.NewRecoverableError(
|
2023-02-02 20:20:26 +00:00
|
|
|
fmt.Errorf("%w: %v; see: <https://www.nomadproject.io/s/envoy-bootstrap-error>",
|
|
|
|
errEnvoyBootstrapError,
|
|
|
|
cmdErr,
|
|
|
|
),
|
2021-04-26 19:24:06 +00:00
|
|
|
recoverable,
|
|
|
|
)
|
2019-08-12 22:41:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2019-12-06 06:49:01 +00:00
|
|
|
|
2022-09-22 18:18:18 +00:00
|
|
|
func (h *envoyBootstrapHook) groupEnv() []string {
|
|
|
|
return []string{
|
|
|
|
fmt.Sprintf("%s=%s", taskenv.AllocID, h.alloc.ID),
|
|
|
|
fmt.Sprintf("%s=%s", taskenv.ShortAllocID, h.alloc.ID[:8]),
|
|
|
|
fmt.Sprintf("%s=%s", taskenv.AllocName, h.alloc.Name),
|
|
|
|
fmt.Sprintf("%s=%s", taskenv.GroupName, h.alloc.TaskGroup),
|
|
|
|
fmt.Sprintf("%s=%s", taskenv.JobName, h.alloc.Job.Name),
|
|
|
|
fmt.Sprintf("%s=%s", taskenv.JobID, h.alloc.Job.ID),
|
|
|
|
fmt.Sprintf("%s=%s", taskenv.Namespace, h.alloc.Namespace),
|
|
|
|
fmt.Sprintf("%s=%s", taskenv.Region, h.alloc.Job.Region),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-09 18:44:02 +00:00
|
|
|
// buildEnvoyAdminBind determines a unique port for use by the envoy admin listener.
|
|
|
|
//
|
|
|
|
// This listener will be bound to 127.0.0.2.
|
|
|
|
func buildEnvoyAdminBind(alloc *structs.Allocation, service, task string, env *taskenv.TaskEnv) string {
|
|
|
|
return buildEnvoyBind(alloc, "127.0.0.2", service, task, env, envoyBaseAdminPort)
|
|
|
|
}
|
|
|
|
|
|
|
|
// buildEnvoyAdminBind determines a unique port for use by the envoy ready listener.
|
|
|
|
//
|
|
|
|
// This listener will be bound to 127.0.0.1.
|
|
|
|
func buildEnvoyReadyBind(alloc *structs.Allocation, service, task string, env *taskenv.TaskEnv) string {
|
|
|
|
return buildEnvoyBind(alloc, "127.0.0.1", service, task, env, envoyBaseReadyPort)
|
|
|
|
}
|
|
|
|
|
|
|
|
// buildEnvoyBind is used to determine a unique port for an envoy listener.
|
2020-07-28 20:12:08 +00:00
|
|
|
//
|
|
|
|
// In bridge mode, if multiple sidecars are running, the bind addresses need
|
2021-07-09 18:44:02 +00:00
|
|
|
// to be unique within the namespace, so we simply start at basePort and increment
|
2020-07-28 20:12:08 +00:00
|
|
|
// by the index of the task.
|
|
|
|
//
|
|
|
|
// In host mode, use the port provided through the service definition, which can
|
|
|
|
// be a port chosen by Nomad.
|
2021-07-09 18:44:02 +00:00
|
|
|
func buildEnvoyBind(alloc *structs.Allocation, ifce, service, task string, taskEnv *taskenv.TaskEnv, basePort int) string {
|
2020-07-28 20:12:08 +00:00
|
|
|
tg := alloc.Job.LookupTaskGroup(alloc.TaskGroup)
|
2021-07-09 18:44:02 +00:00
|
|
|
port := basePort
|
2020-07-28 20:12:08 +00:00
|
|
|
switch tg.Networks[0].Mode {
|
|
|
|
case "host":
|
2021-03-01 08:39:14 +00:00
|
|
|
interpolatedServices := taskenv.InterpolateServices(taskEnv, tg.Services)
|
2021-07-09 18:44:02 +00:00
|
|
|
for _, svc := range interpolatedServices {
|
|
|
|
if svc.Name == service {
|
|
|
|
mapping := tg.Networks.Port(svc.PortLabel)
|
2021-02-09 12:05:28 +00:00
|
|
|
port = mapping.Value
|
2020-07-28 20:12:08 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
default:
|
2021-07-09 18:44:02 +00:00
|
|
|
for idx, tgTask := range tg.Tasks {
|
|
|
|
if tgTask.Name == task {
|
2020-07-28 20:12:08 +00:00
|
|
|
port += idx
|
|
|
|
break
|
|
|
|
}
|
2019-12-06 06:49:01 +00:00
|
|
|
}
|
|
|
|
}
|
2021-07-12 14:04:54 +00:00
|
|
|
return net.JoinHostPort(ifce, strconv.Itoa(port))
|
2019-12-06 06:49:01 +00:00
|
|
|
}
|
2019-12-18 16:23:16 +00:00
|
|
|
|
|
|
|
func (h *envoyBootstrapHook) writeConfig(filename, config string) error {
|
2023-03-08 19:25:10 +00:00
|
|
|
if err := os.WriteFile(filename, []byte(config), 0440); err != nil {
|
2019-12-18 16:23:16 +00:00
|
|
|
_ = os.Remove(filename)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-07-28 20:12:08 +00:00
|
|
|
// grpcAddress determines the Consul gRPC endpoint address to use.
|
|
|
|
//
|
|
|
|
// In host networking this will default to 127.0.0.1:8502.
|
|
|
|
// In bridge/cni networking this will default to unix://<socket>.
|
|
|
|
// In either case, CONSUL_GRPC_ADDR will override the default.
|
|
|
|
func (h *envoyBootstrapHook) grpcAddress(env map[string]string) string {
|
|
|
|
if address := env[grpcConsulVariable]; address != "" {
|
|
|
|
return address
|
|
|
|
}
|
|
|
|
|
|
|
|
tg := h.alloc.Job.LookupTaskGroup(h.alloc.TaskGroup)
|
|
|
|
switch tg.Networks[0].Mode {
|
|
|
|
case "host":
|
|
|
|
return grpcDefaultAddress
|
|
|
|
default:
|
|
|
|
return "unix://" + allocdir.AllocGRPCSocket
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-19 15:46:44 +00:00
|
|
|
func (h *envoyBootstrapHook) proxyServiceID(group string, service *structs.Service) string {
|
2022-03-15 08:38:30 +00:00
|
|
|
// Note, it is critical the ID here matches what is actually registered in
|
|
|
|
// Consul. See: WorkloadServices.Name in serviceregistration/workload.go.
|
|
|
|
return serviceregistration.MakeAllocServiceID(h.alloc.ID, "group-"+group, service)
|
2021-01-19 15:46:44 +00:00
|
|
|
}
|
|
|
|
|
2021-04-12 19:10:10 +00:00
|
|
|
// newEnvoyBootstrapArgs is used to prepare for the invocation of the
|
|
|
|
// 'consul connect envoy' command with arguments which will bootstrap the connect
|
|
|
|
// proxy or gateway.
|
|
|
|
//
|
|
|
|
// https://www.consul.io/commands/connect/envoy#consul-connect-envoy
|
2020-07-28 20:12:08 +00:00
|
|
|
func (h *envoyBootstrapHook) newEnvoyBootstrapArgs(
|
2021-01-19 15:46:44 +00:00
|
|
|
group string, service *structs.Service,
|
2021-07-09 18:44:02 +00:00
|
|
|
grpcAddr, envoyAdminBind, envoyReadyBind, siToken, filepath string,
|
2020-07-28 20:12:08 +00:00
|
|
|
) envoyBootstrapArgs {
|
|
|
|
|
2022-02-04 16:49:15 +00:00
|
|
|
namespace := h.getConsulNamespace()
|
|
|
|
proxyID := h.proxyServiceID(group, service)
|
2021-03-16 18:22:21 +00:00
|
|
|
|
2022-02-04 16:49:15 +00:00
|
|
|
var gateway string
|
2020-12-15 20:38:33 +00:00
|
|
|
switch {
|
|
|
|
case service.Connect.HasSidecar():
|
2022-02-04 16:49:15 +00:00
|
|
|
proxyID += "-sidecar-proxy"
|
2020-12-15 20:38:33 +00:00
|
|
|
case service.Connect.IsIngress():
|
|
|
|
gateway = "ingress"
|
|
|
|
case service.Connect.IsTerminating():
|
|
|
|
gateway = "terminating"
|
2021-04-12 19:10:10 +00:00
|
|
|
case service.Connect.IsMesh():
|
|
|
|
gateway = "mesh"
|
2020-07-28 20:12:08 +00:00
|
|
|
}
|
|
|
|
|
2021-04-12 19:10:10 +00:00
|
|
|
h.logger.Info("bootstrapping envoy",
|
2022-02-04 16:49:15 +00:00
|
|
|
"namespace", namespace, "proxy_id", proxyID, "service", service.Name,
|
|
|
|
"gateway", gateway, "bootstrap_file", filepath, "grpc_addr", grpcAddr,
|
2021-07-09 18:44:02 +00:00
|
|
|
"admin_bind", envoyAdminBind, "ready_bind", envoyReadyBind,
|
2020-07-28 20:12:08 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
return envoyBootstrapArgs{
|
|
|
|
consulConfig: h.consulConfig,
|
|
|
|
grpcAddr: grpcAddr,
|
|
|
|
envoyAdminBind: envoyAdminBind,
|
2021-07-09 18:44:02 +00:00
|
|
|
envoyReadyBind: envoyReadyBind,
|
2020-07-28 20:12:08 +00:00
|
|
|
siToken: siToken,
|
|
|
|
gateway: gateway,
|
2021-01-19 15:46:44 +00:00
|
|
|
proxyID: proxyID,
|
2021-03-16 18:22:21 +00:00
|
|
|
namespace: namespace,
|
2020-07-28 20:12:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-15 15:29:47 +00:00
|
|
|
// envoyBootstrapArgs is used to accumulate CLI arguments that will be passed
|
|
|
|
// along to the exec invocation of consul which will then generate the bootstrap
|
|
|
|
// configuration file for envoy.
|
2019-12-18 16:23:16 +00:00
|
|
|
type envoyBootstrapArgs struct {
|
2020-05-13 20:15:55 +00:00
|
|
|
consulConfig consulTransportConfig
|
2019-12-18 16:23:16 +00:00
|
|
|
grpcAddr string
|
|
|
|
envoyAdminBind string
|
2021-07-09 18:44:02 +00:00
|
|
|
envoyReadyBind string
|
2019-12-18 16:23:16 +00:00
|
|
|
siToken string
|
2020-07-28 20:12:08 +00:00
|
|
|
gateway string // gateways only
|
2022-02-04 16:49:15 +00:00
|
|
|
proxyID string // gateways and sidecars
|
2021-03-16 18:22:21 +00:00
|
|
|
namespace string
|
2019-12-18 16:23:16 +00:00
|
|
|
}
|
|
|
|
|
2020-01-15 15:29:47 +00:00
|
|
|
// args returns the CLI arguments consul needs in the correct order, with the
|
|
|
|
// -token argument present or not present depending on whether it is set.
|
2019-12-18 16:23:16 +00:00
|
|
|
func (e envoyBootstrapArgs) args() []string {
|
|
|
|
arguments := []string{
|
|
|
|
"connect",
|
|
|
|
"envoy",
|
|
|
|
"-grpc-addr", e.grpcAddr,
|
2020-04-02 16:30:38 +00:00
|
|
|
"-http-addr", e.consulConfig.HTTPAddr,
|
2020-08-26 18:17:16 +00:00
|
|
|
"-admin-bind", e.envoyAdminBind,
|
2021-07-09 18:44:02 +00:00
|
|
|
"-address", e.envoyReadyBind,
|
2022-02-04 16:49:15 +00:00
|
|
|
"-proxy-id", e.proxyID,
|
2019-12-18 16:23:16 +00:00
|
|
|
"-bootstrap",
|
2020-07-28 20:12:08 +00:00
|
|
|
}
|
|
|
|
|
2023-01-11 15:34:28 +00:00
|
|
|
appendIfSet := func(param, value string) {
|
|
|
|
if value != "" {
|
|
|
|
arguments = append(arguments, param, value)
|
|
|
|
}
|
2019-12-18 16:23:16 +00:00
|
|
|
}
|
2020-04-02 16:30:38 +00:00
|
|
|
|
2023-01-11 15:34:28 +00:00
|
|
|
appendIfSet("-gateway", e.gateway)
|
|
|
|
appendIfSet("-token", e.siToken)
|
|
|
|
appendIfSet("-grpc-ca-file", e.consulConfig.GRPCCAFile)
|
|
|
|
appendIfSet("-ca-file", e.consulConfig.CAFile)
|
|
|
|
appendIfSet("-client-cert", e.consulConfig.CertFile)
|
|
|
|
appendIfSet("-client-key", e.consulConfig.KeyFile)
|
|
|
|
appendIfSet("-namespace", e.namespace)
|
2020-10-02 18:46:36 +00:00
|
|
|
|
2019-12-18 16:23:16 +00:00
|
|
|
return arguments
|
|
|
|
}
|
|
|
|
|
2020-04-02 16:30:38 +00:00
|
|
|
// env creates the context of environment variables to be used when exec-ing
|
|
|
|
// the consul command for generating the envoy bootstrap config. It is expected
|
|
|
|
// the value of os.Environ() is passed in to be appended to. Because these are
|
|
|
|
// appended at the end of what will be passed into Cmd.Env, they will override
|
|
|
|
// any pre-existing values (i.e. what the Nomad agent was launched with).
|
|
|
|
// https://golang.org/pkg/os/exec/#Cmd
|
|
|
|
func (e envoyBootstrapArgs) env(env []string) []string {
|
|
|
|
if v := e.consulConfig.Auth; v != "" {
|
|
|
|
env = append(env, fmt.Sprintf("%s=%s", "CONSUL_HTTP_AUTH", v))
|
|
|
|
}
|
|
|
|
if v := e.consulConfig.SSL; v != "" {
|
|
|
|
env = append(env, fmt.Sprintf("%s=%s", "CONSUL_HTTP_SSL", v))
|
|
|
|
}
|
|
|
|
if v := e.consulConfig.VerifySSL; v != "" {
|
|
|
|
env = append(env, fmt.Sprintf("%s=%s", "CONSUL_HTTP_SSL_VERIFY", v))
|
|
|
|
}
|
2021-03-16 18:22:21 +00:00
|
|
|
if v := e.namespace; v != "" {
|
2020-10-02 18:46:36 +00:00
|
|
|
env = append(env, fmt.Sprintf("%s=%s", "CONSUL_NAMESPACE", v))
|
|
|
|
}
|
2020-04-02 16:30:38 +00:00
|
|
|
return env
|
|
|
|
}
|
|
|
|
|
2020-01-15 14:41:23 +00:00
|
|
|
// maybeLoadSIToken reads the SI token saved to disk in the secrets directory
|
2019-12-18 16:23:16 +00:00
|
|
|
// by the service identities prestart hook. This envoy bootstrap hook blocks
|
|
|
|
// until the sids hook completes, so if the SI token is required to exist (i.e.
|
|
|
|
// Consul ACLs are enabled), it will be in place by the time we try to read it.
|
|
|
|
func (h *envoyBootstrapHook) maybeLoadSIToken(task, dir string) (string, error) {
|
|
|
|
tokenPath := filepath.Join(dir, sidsTokenFile)
|
2023-03-08 19:25:10 +00:00
|
|
|
token, err := os.ReadFile(tokenPath)
|
2019-12-18 16:23:16 +00:00
|
|
|
if err != nil {
|
|
|
|
if !os.IsNotExist(err) {
|
|
|
|
h.logger.Error("failed to load SI token", "task", task, "error", err)
|
2022-04-02 00:24:02 +00:00
|
|
|
return "", fmt.Errorf("failed to load SI token for %s: %w", task, err)
|
2019-12-18 16:23:16 +00:00
|
|
|
}
|
|
|
|
h.logger.Trace("no SI token to load", "task", task)
|
|
|
|
return "", nil // token file does not exist
|
|
|
|
}
|
|
|
|
h.logger.Trace("recovered pre-existing SI token", "task", task)
|
|
|
|
return string(token), nil
|
|
|
|
}
|