Skip to content

Commit

Permalink
consul/connect: dynamically select envoy sidecar at runtime
Browse files Browse the repository at this point in the history
As newer versions of Consul are released, the minimum version of Envoy
it supports as a sidecar proxy also gets bumped. Starting with the upcoming
Consul v1.9.X series, Envoy v1.11.X will no longer be supported. Current
versions of Nomad hardcode a version of Envoy v1.11.2 to be used as the
default implementation of Connect sidecar proxy.

This PR introduces a change such that each Nomad Client will query its
local Consul for a list of Envoy proxies that it supports (hashicorp/consul#8545)
and then launch the Connect sidecar proxy task using the latest supported version
of Envoy. If the API component is not available, Nomad will fallback to
the old version of Envoy supported by old versions of Consul.

Setting the meta configuration option `meta.connect.sidecar_image` or
setting the `connect.sidecar_task` stanza will take precedence as is
the current behavior.

Addresses #8585 #7665
  • Loading branch information
shoenig committed Sep 16, 2020
1 parent 4ba3afa commit 8f899ed
Show file tree
Hide file tree
Showing 22 changed files with 359 additions and 38 deletions.
10 changes: 8 additions & 2 deletions client/allocrunner/alloc_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ type allocRunner struct {
// registering services and checks
consulClient consul.ConsulServiceAPI

// consulProxiesClient is the client used by the envoy version hook for
// looking up supported envoy versions of the consul agent.
consulProxiesClient consul.SupportedProxiesAPI

// sidsClient is the client used by the service identity hook for
// managing SI tokens
sidsClient consul.ServiceIdentityAPI
Expand Down Expand Up @@ -186,6 +190,7 @@ func NewAllocRunner(config *Config) (*allocRunner, error) {
alloc: alloc,
clientConfig: config.ClientConfig,
consulClient: config.Consul,
consulProxiesClient: config.ConsulProxies,
sidsClient: config.ConsulSI,
vaultClient: config.Vault,
tasks: make(map[string]*taskrunner.TaskRunner, len(tg.Tasks)),
Expand Down Expand Up @@ -236,7 +241,7 @@ func NewAllocRunner(config *Config) (*allocRunner, error) {
// initTaskRunners creates task runners but does *not* run them.
func (ar *allocRunner) initTaskRunners(tasks []*structs.Task) error {
for _, task := range tasks {
config := &taskrunner.Config{
trConfig := &taskrunner.Config{
Alloc: ar.alloc,
ClientConfig: ar.clientConfig,
Task: task,
Expand All @@ -246,6 +251,7 @@ func (ar *allocRunner) initTaskRunners(tasks []*structs.Task) error {
StateUpdater: ar,
DynamicRegistry: ar.dynamicRegistry,
Consul: ar.consulClient,
ConsulProxies: ar.consulProxiesClient,
ConsulSI: ar.sidsClient,
Vault: ar.vaultClient,
DeviceStatsReporter: ar.deviceStatsReporter,
Expand All @@ -257,7 +263,7 @@ func (ar *allocRunner) initTaskRunners(tasks []*structs.Task) error {
}

// Create, but do not Run, the task runner
tr, err := taskrunner.NewTaskRunner(config)
tr, err := taskrunner.NewTaskRunner(trConfig)
if err != nil {
return fmt.Errorf("failed creating runner for task %q: %v", task.Name, err)
}
Expand Down
4 changes: 4 additions & 0 deletions client/allocrunner/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ type Config struct {
// Consul is the Consul client used to register task services and checks
Consul consul.ConsulServiceAPI

// ConsulProxies is the Consul client used to lookup supported envoy versions
// of the Consul agent.
ConsulProxies consul.SupportedProxiesAPI

// ConsulSI is the Consul client used to manage service identity tokens.
ConsulSI consul.ServiceIdentityAPI

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad/client/allocdir"
"github.com/hashicorp/nomad/client/allocrunner/interfaces"
ifs "github.com/hashicorp/nomad/client/allocrunner/interfaces"
agentconsul "github.com/hashicorp/nomad/command/agent/consul"
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/nomad/structs"
Expand Down Expand Up @@ -150,7 +150,7 @@ func (h *envoyBootstrapHook) lookupService(svcKind, svcName, tgName string) (*st
// Prestart creates an envoy bootstrap config file.
//
// Must be aware of both launching envoy as a sidecar proxy, as well as a connect gateway.
func (h *envoyBootstrapHook) Prestart(ctx context.Context, req *interfaces.TaskPrestartRequest, resp *interfaces.TaskPrestartResponse) error {
func (h *envoyBootstrapHook) Prestart(ctx context.Context, req *ifs.TaskPrestartRequest, resp *ifs.TaskPrestartResponse) error {
if !req.Task.Kind.IsConnectProxy() && !req.Task.Kind.IsAnyConnectGateway() {
// Not a Connect proxy sidecar
resp.Done = true
Expand Down
145 changes: 145 additions & 0 deletions client/allocrunner/taskrunner/envoy_version_hook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package taskrunner

import (
"context"
"fmt"

hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-version"
ifs "github.com/hashicorp/nomad/client/allocrunner/interfaces"
"github.com/hashicorp/nomad/client/consul"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/pkg/errors"
)

const (
// envoyVersionHookName is the name of this hook and appears in logs.
envoyVersionHookName = "envoy_version"

// envoyLegacyImage is used when the version of Consul is too old to support
// the SupportedProxies field in the self API.
//
// This is the version defaulted by Nomad before v1.0.
envoyLegacyImage = "envoyproxy/envoy:v1.11.2@sha256:a7769160c9c1a55bb8d07a3b71ce5d64f72b1f665f10d81aa1581bc3cf850d09"

// envoyImageFormat is the format string used for official envoy Docker images
// with the tag being the semver of the version of envoy.
envoyImageFormat = "envoyproxy/envoy:%s"
)

type envoyVersionHookConfig struct {
alloc *structs.Allocation
proxiesClient consul.SupportedProxiesAPI
logger hclog.Logger
}

func newEnvoyVersionHookConfig(alloc *structs.Allocation, proxiesClient consul.SupportedProxiesAPI, logger hclog.Logger) *envoyVersionHookConfig {
return &envoyVersionHookConfig{
alloc: alloc,
logger: logger,
proxiesClient: proxiesClient,
}
}

type envoyVersionHook struct {
// alloc is the allocation with the envoy task being rewritten.
alloc *structs.Allocation

// proxiesClient is the subset of the Consul API for getting information
// from Consul about the versions of Envoy it supports.
proxiesClient consul.SupportedProxiesAPI

// logger is used to log things.
logger hclog.Logger
}

func newEnvoyVersionHook(c *envoyVersionHookConfig) *envoyVersionHook {
return &envoyVersionHook{
alloc: c.alloc,
proxiesClient: c.proxiesClient,
logger: c.logger.Named(envoyVersionHookName),
}
}

func (envoyVersionHook) Name() string {
return envoyVersionHookName
}

func (h *envoyVersionHook) Prestart(ctx context.Context, request *ifs.TaskPrestartRequest, response *ifs.TaskPrestartResponse) error {
if h.skip(request) {
response.Done = true
return nil
}

// it's either legacy or manageable, need to know consul version
proxies, err := h.proxiesClient.Proxies()
if err != nil {
return err
}

image, err := h.image(proxies)
if err != nil {
return err
}

h.logger.Trace("setting task envoy image", "image", image)
request.Task.Config["image"] = image
response.Done = true
return nil
}

// skip will return true if the request does not contain a task that should have
// its envoy proxy version resolved automatically.
func (h *envoyVersionHook) skip(request *ifs.TaskPrestartRequest) bool {
switch {
case request.Task.Driver != "docker":
return true
case !request.Task.UsesConnectSidecar():
return true
case !h.isSentinel(request.Task.Config):
return true
}
return false
}

// isSentinel returns true if the docker.config.image value has been left to
// Nomad's default sentinel value, indicating that Nomad and Consul should work
// together to determine the best Envoy version to use.
func (_ *envoyVersionHook) isSentinel(config map[string]interface{}) bool {
if len(config) == 0 {
return false
}

image, ok := config["image"].(string)
if !ok {
return false
}

return image == structs.ConnectEnvoySentinel
}

// image determines the best Envoy version to use. if supported is nil or empty
// Nomad will fallback to the legacy envoy image used before Nomad v1.0.
func (_ *envoyVersionHook) image(supported map[string][]string) (string, error) {
versions := supported["envoy"]
if len(versions) == 0 {
return envoyLegacyImage, nil
}

latest, err := semver(versions[0])
if err != nil {
return "", err
}

return fmt.Sprintf(envoyImageFormat, latest), nil
}

// semver sanitizes the envoy version string coming from Consul into the format
// used by the Envoy project when publishing images (i.e. proper semver).
func semver(chosen string) (string, error) {
v, err := version.NewVersion(chosen)
if err != nil {
return "", errors.Wrap(err, "unexpected envoy version format")
}
return "v" + v.String(), nil
}
14 changes: 12 additions & 2 deletions client/allocrunner/taskrunner/task_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,12 @@ type TaskRunner struct {

// consulClient is the client used by the consul service hook for
// registering services and checks
consulClient consul.ConsulServiceAPI
consulServiceClient consul.ConsulServiceAPI

// consulProxiesClient is the client used by the envoy version hook for
// asking consul what version of envoy nomad should inject into the connect
// sidecar or gateway task.
consulProxiesClient consul.SupportedProxiesAPI

// sidsClient is the client used by the service identity hook for managing
// service identity tokens
Expand Down Expand Up @@ -234,6 +239,10 @@ type Config struct {
// Consul is the client to use for managing Consul service registrations
Consul consul.ConsulServiceAPI

// ConsulProxies is the client to use for looking up supported envoy versions
// from Consul.
ConsulProxies consul.SupportedProxiesAPI

// ConsulSI is the client to use for managing Consul SI tokens
ConsulSI consul.ServiceIdentityAPI

Expand Down Expand Up @@ -302,7 +311,8 @@ func NewTaskRunner(config *Config) (*TaskRunner, error) {
taskLeader: config.Task.Leader,
envBuilder: envBuilder,
dynamicRegistry: config.DynamicRegistry,
consulClient: config.Consul,
consulServiceClient: config.Consul,
consulProxiesClient: config.ConsulProxies,
siClient: config.ConsulSI,
vaultClient: config.Vault,
state: tstate,
Expand Down
13 changes: 7 additions & 6 deletions client/allocrunner/taskrunner/task_runner_hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func (tr *TaskRunner) initHooks() {
tr.runnerHooks = append(tr.runnerHooks, newServiceHook(serviceHookConfig{
alloc: tr.Alloc(),
task: tr.Task(),
consul: tr.consulClient,
consul: tr.consulServiceClient,
restarter: tr,
logger: hookLogger,
}))
Expand All @@ -127,10 +127,11 @@ func (tr *TaskRunner) initHooks() {
}))
}

if task.Kind.IsConnectProxy() || task.Kind.IsAnyConnectGateway() {
tr.runnerHooks = append(tr.runnerHooks, newEnvoyBootstrapHook(
newEnvoyBootstrapHookConfig(alloc, tr.clientConfig.ConsulConfig, hookLogger),
))
if task.UsesConnectSidecar() {
tr.runnerHooks = append(tr.runnerHooks,
newEnvoyVersionHook(newEnvoyVersionHookConfig(alloc, tr.consulProxiesClient, hookLogger)),
newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(alloc, tr.clientConfig.ConsulConfig, hookLogger)),
)
} else if task.Kind.IsConnectNative() {
tr.runnerHooks = append(tr.runnerHooks, newConnectNativeHook(
newConnectNativeHookConfig(alloc, tr.clientConfig.ConsulConfig, hookLogger),
Expand All @@ -142,7 +143,7 @@ func (tr *TaskRunner) initHooks() {
scriptCheckHook := newScriptCheckHook(scriptCheckHookConfig{
alloc: tr.Alloc(),
task: tr.Task(),
consul: tr.consulClient,
consul: tr.consulServiceClient,
logger: hookLogger,
})
tr.runnerHooks = append(tr.runnerHooks, scriptCheckHook)
Expand Down
8 changes: 7 additions & 1 deletion client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ type Client struct {
// and checks.
consulService consulApi.ConsulServiceAPI

// consulProxies is Nomad's custom Consul client for looking up supported
// envoy versions
consulProxies consulApi.SupportedProxiesAPI

// consulCatalog is the subset of Consul's Catalog API Nomad uses.
consulCatalog consul.CatalogAPI

Expand Down Expand Up @@ -306,7 +310,7 @@ var (
)

// NewClient is used to create a new client from the given configuration
func NewClient(cfg *config.Config, consulCatalog consul.CatalogAPI, consulService consulApi.ConsulServiceAPI) (*Client, error) {
func NewClient(cfg *config.Config, consulCatalog consul.CatalogAPI, consulProxies consulApi.SupportedProxiesAPI, consulService consulApi.ConsulServiceAPI) (*Client, error) {
// Create the tls wrapper
var tlsWrap tlsutil.RegionWrapper
if cfg.TLSConfig.EnableRPC {
Expand All @@ -331,6 +335,7 @@ func NewClient(cfg *config.Config, consulCatalog consul.CatalogAPI, consulServic
c := &Client{
config: cfg,
consulCatalog: consulCatalog,
consulProxies: consulProxies,
consulService: consulService,
start: time.Now(),
connPool: pool.NewPool(logger, clientRPCCache, clientMaxStreams, tlsWrap),
Expand Down Expand Up @@ -2382,6 +2387,7 @@ func (c *Client) addAlloc(alloc *structs.Allocation, migrateToken string) error
ClientConfig: c.configCopy,
StateDB: c.stateDB,
Consul: c.consulService,
ConsulProxies: c.consulProxies,
ConsulSI: c.tokensClient,
Vault: c.vaultClient,
StateUpdater: c,
Expand Down
2 changes: 1 addition & 1 deletion client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,7 @@ func TestClient_SaveRestoreState(t *testing.T) {
c1.config.PluginLoader = catalog.TestPluginLoaderWithOptions(t, "", c1.config.Options, nil)
c1.config.PluginSingletonLoader = singleton.NewSingletonLoader(logger, c1.config.PluginLoader)

c2, err := NewClient(c1.config, consulCatalog, mockService)
c2, err := NewClient(c1.config, consulCatalog, nil, mockService) // todo(shoenig)
if err != nil {
t.Fatalf("err: %v", err)
}
Expand Down
14 changes: 7 additions & 7 deletions client/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/helper/pluginutils/loader"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/nomad/structs/config"
structsc "github.com/hashicorp/nomad/nomad/structs/config"
"github.com/hashicorp/nomad/plugins/base"
"github.com/hashicorp/nomad/version"
)
Expand Down Expand Up @@ -144,10 +144,10 @@ type Config struct {
Version *version.VersionInfo

// ConsulConfig is this Agent's Consul configuration
ConsulConfig *config.ConsulConfig
ConsulConfig *structsc.ConsulConfig

// VaultConfig is this Agent's Vault configuration
VaultConfig *config.VaultConfig
VaultConfig *structsc.VaultConfig

// StatsCollectionInterval is the interval at which the Nomad client
// collects resource usage stats
Expand All @@ -162,7 +162,7 @@ type Config struct {
PublishAllocationMetrics bool

// TLSConfig holds various TLS related configurations
TLSConfig *config.TLSConfig
TLSConfig *structsc.TLSConfig

// GCInterval is the time interval at which the client triggers garbage
// collection
Expand Down Expand Up @@ -303,12 +303,12 @@ func (c *Config) Copy() *Config {
func DefaultConfig() *Config {
return &Config{
Version: version.GetVersion(),
VaultConfig: config.DefaultVaultConfig(),
ConsulConfig: config.DefaultConsulConfig(),
VaultConfig: structsc.DefaultVaultConfig(),
ConsulConfig: structsc.DefaultConsulConfig(),
LogOutput: os.Stderr,
Region: "global",
StatsCollectionInterval: 1 * time.Second,
TLSConfig: &config.TLSConfig{},
TLSConfig: &structsc.TLSConfig{},
LogLevel: "DEBUG",
GCInterval: 1 * time.Minute,
GCParallelDestroys: 2,
Expand Down
8 changes: 8 additions & 0 deletions client/consul/consul.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,11 @@ type ServiceIdentityAPI interface {
// identity tokens be generated for tasks in the allocation.
DeriveSITokens(alloc *structs.Allocation, tasks []string) (map[string]string, error)
}

// SupportedProxiesAPI is the interface the Nomad Client uses to request from
// Consul the set of supported proxied to use for Consul Connect.
//
// No ACL requirements
type SupportedProxiesAPI interface {
Proxies() (map[string][]string, error)
}
Loading

0 comments on commit 8f899ed

Please sign in to comment.