Skip to content

Commit 2214381

Browse files
authored
Merge pull request #8945 from hashicorp/f-auto-sidecar
consul/connect: dynamically select envoy sidecar at runtime
2 parents 405e9d8 + bdeb73c commit 2214381

28 files changed

+850
-42
lines changed

client/allocrunner/alloc_runner.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ type allocRunner struct {
6464
// registering services and checks
6565
consulClient consul.ConsulServiceAPI
6666

67+
// consulProxiesClient is the client used by the envoy version hook for
68+
// looking up supported envoy versions of the consul agent.
69+
consulProxiesClient consul.SupportedProxiesAPI
70+
6771
// sidsClient is the client used by the service identity hook for
6872
// managing SI tokens
6973
sidsClient consul.ServiceIdentityAPI
@@ -186,6 +190,7 @@ func NewAllocRunner(config *Config) (*allocRunner, error) {
186190
alloc: alloc,
187191
clientConfig: config.ClientConfig,
188192
consulClient: config.Consul,
193+
consulProxiesClient: config.ConsulProxies,
189194
sidsClient: config.ConsulSI,
190195
vaultClient: config.Vault,
191196
tasks: make(map[string]*taskrunner.TaskRunner, len(tg.Tasks)),
@@ -236,7 +241,7 @@ func NewAllocRunner(config *Config) (*allocRunner, error) {
236241
// initTaskRunners creates task runners but does *not* run them.
237242
func (ar *allocRunner) initTaskRunners(tasks []*structs.Task) error {
238243
for _, task := range tasks {
239-
config := &taskrunner.Config{
244+
trConfig := &taskrunner.Config{
240245
Alloc: ar.alloc,
241246
ClientConfig: ar.clientConfig,
242247
Task: task,
@@ -246,6 +251,7 @@ func (ar *allocRunner) initTaskRunners(tasks []*structs.Task) error {
246251
StateUpdater: ar,
247252
DynamicRegistry: ar.dynamicRegistry,
248253
Consul: ar.consulClient,
254+
ConsulProxies: ar.consulProxiesClient,
249255
ConsulSI: ar.sidsClient,
250256
Vault: ar.vaultClient,
251257
DeviceStatsReporter: ar.deviceStatsReporter,
@@ -257,7 +263,7 @@ func (ar *allocRunner) initTaskRunners(tasks []*structs.Task) error {
257263
}
258264

259265
// Create, but do not Run, the task runner
260-
tr, err := taskrunner.NewTaskRunner(config)
266+
tr, err := taskrunner.NewTaskRunner(trConfig)
261267
if err != nil {
262268
return fmt.Errorf("failed creating runner for task %q: %v", task.Name, err)
263269
}

client/allocrunner/config.go

+4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ type Config struct {
3232
// Consul is the Consul client used to register task services and checks
3333
Consul consul.ConsulServiceAPI
3434

35+
// ConsulProxies is the Consul client used to lookup supported envoy versions
36+
// of the Consul agent.
37+
ConsulProxies consul.SupportedProxiesAPI
38+
3539
// ConsulSI is the Consul client used to manage service identity tokens.
3640
ConsulSI consul.ServiceIdentityAPI
3741

client/allocrunner/taskrunner/envoybootstrap_hook.go client/allocrunner/taskrunner/envoy_bootstrap_hook.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212

1313
"github.com/hashicorp/go-hclog"
1414
"github.com/hashicorp/nomad/client/allocdir"
15-
"github.com/hashicorp/nomad/client/allocrunner/interfaces"
15+
ifs "github.com/hashicorp/nomad/client/allocrunner/interfaces"
1616
agentconsul "github.com/hashicorp/nomad/command/agent/consul"
1717
"github.com/hashicorp/nomad/helper"
1818
"github.com/hashicorp/nomad/nomad/structs"
@@ -152,7 +152,7 @@ func (h *envoyBootstrapHook) lookupService(svcKind, svcName, tgName string) (*st
152152
// Prestart creates an envoy bootstrap config file.
153153
//
154154
// Must be aware of both launching envoy as a sidecar proxy, as well as a connect gateway.
155-
func (h *envoyBootstrapHook) Prestart(ctx context.Context, req *interfaces.TaskPrestartRequest, resp *interfaces.TaskPrestartResponse) error {
155+
func (h *envoyBootstrapHook) Prestart(ctx context.Context, req *ifs.TaskPrestartRequest, resp *ifs.TaskPrestartResponse) error {
156156
if !req.Task.Kind.IsConnectProxy() && !req.Task.Kind.IsAnyConnectGateway() {
157157
// Not a Connect proxy sidecar
158158
resp.Done = true

client/allocrunner/taskrunner/envoybootstrap_hook_test.go client/allocrunner/taskrunner/envoy_bootstrap_hook_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ func TestTaskRunner_EnvoyBootstrapHook_gateway_ok(t *testing.T) {
499499
// Run the hook
500500
require.NoError(t, h.Prestart(context.Background(), req, &resp))
501501

502-
// Assert the hook is done
502+
// Assert the hook is Done
503503
require.True(t, resp.Done)
504504
require.NotNil(t, resp.Env)
505505

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package taskrunner
2+
3+
import (
4+
"context"
5+
"strings"
6+
7+
"github.com/hashicorp/go-hclog"
8+
"github.com/hashicorp/go-version"
9+
ifs "github.com/hashicorp/nomad/client/allocrunner/interfaces"
10+
"github.com/hashicorp/nomad/client/consul"
11+
"github.com/hashicorp/nomad/nomad/structs"
12+
"github.com/pkg/errors"
13+
)
14+
15+
const (
16+
// envoyVersionHookName is the name of this hook and appears in logs.
17+
envoyVersionHookName = "envoy_version"
18+
19+
// envoyLegacyImage is used when the version of Consul is too old to support
20+
// the SupportedProxies field in the self API.
21+
//
22+
// This is the version defaulted by Nomad before v0.13.0 and/or when using versions
23+
// of Consul before v1.7.8, v1.8.5, and v1.9.0.
24+
envoyLegacyImage = "envoyproxy/envoy:v1.11.2@sha256:a7769160c9c1a55bb8d07a3b71ce5d64f72b1f665f10d81aa1581bc3cf850d09"
25+
)
26+
27+
type envoyVersionHookConfig struct {
28+
alloc *structs.Allocation
29+
proxiesClient consul.SupportedProxiesAPI
30+
logger hclog.Logger
31+
}
32+
33+
func newEnvoyVersionHookConfig(alloc *structs.Allocation, proxiesClient consul.SupportedProxiesAPI, logger hclog.Logger) *envoyVersionHookConfig {
34+
return &envoyVersionHookConfig{
35+
alloc: alloc,
36+
logger: logger,
37+
proxiesClient: proxiesClient,
38+
}
39+
}
40+
41+
// envoyVersionHook is used to determine and set the Docker image used for Consul
42+
// Connect sidecar proxy tasks. It will query Consul for a set of preferred Envoy
43+
// versions if the task image is unset or references ${NOMAD_envoy_version}. Nomad
44+
// will fallback the image to the previous default Envoy v1.11.2 if Consul is too old
45+
// to support the supported proxies API.
46+
type envoyVersionHook struct {
47+
// alloc is the allocation with the envoy task being rewritten.
48+
alloc *structs.Allocation
49+
50+
// proxiesClient is the subset of the Consul API for getting information
51+
// from Consul about the versions of Envoy it supports.
52+
proxiesClient consul.SupportedProxiesAPI
53+
54+
// logger is used to log things.
55+
logger hclog.Logger
56+
}
57+
58+
func newEnvoyVersionHook(c *envoyVersionHookConfig) *envoyVersionHook {
59+
return &envoyVersionHook{
60+
alloc: c.alloc,
61+
proxiesClient: c.proxiesClient,
62+
logger: c.logger.Named(envoyVersionHookName),
63+
}
64+
}
65+
66+
func (envoyVersionHook) Name() string {
67+
return envoyVersionHookName
68+
}
69+
70+
func (h *envoyVersionHook) Prestart(_ context.Context, request *ifs.TaskPrestartRequest, response *ifs.TaskPrestartResponse) error {
71+
if h.skip(request) {
72+
response.Done = true
73+
return nil
74+
}
75+
76+
// We either need to acquire Consul's preferred Envoy version or fallback
77+
// to the legacy default. Query Consul and use the (possibly empty) result.
78+
proxies, err := h.proxiesClient.Proxies()
79+
if err != nil {
80+
return errors.Wrap(err, "error retrieving supported Envoy versions from Consul")
81+
}
82+
83+
// Determine the concrete Envoy image identifier by applying version string
84+
// substitution (${NOMAD_envoy_version}).
85+
image, err := h.tweakImage(h.taskImage(request.Task.Config), proxies)
86+
if err != nil {
87+
return errors.Wrap(err, "error interpreting desired Envoy version from Consul")
88+
}
89+
90+
// Set the resulting image.
91+
h.logger.Trace("setting task envoy image", "image", image)
92+
request.Task.Config["image"] = image
93+
response.Done = true
94+
return nil
95+
}
96+
97+
// skip will return true if the request does not contain a task that should have
98+
// its envoy proxy version resolved automatically.
99+
func (h *envoyVersionHook) skip(request *ifs.TaskPrestartRequest) bool {
100+
switch {
101+
case request.Task.Driver != "docker":
102+
return true
103+
case !request.Task.UsesConnectSidecar():
104+
return true
105+
case !h.needsVersion(request.Task.Config):
106+
return true
107+
}
108+
return false
109+
}
110+
111+
// getConfiguredImage extracts the configured config.image value from the request.
112+
// If the image is empty or not a string, Nomad will fallback to the normal
113+
// official Envoy image as if the setting was not configured. This is also what
114+
// Nomad would do if the sidecar_task was not set in the first place.
115+
func (_ *envoyVersionHook) taskImage(config map[string]interface{}) string {
116+
value, exists := config["image"]
117+
if !exists {
118+
return structs.EnvoyImageFormat
119+
}
120+
121+
image, ok := value.(string)
122+
if !ok {
123+
return structs.EnvoyImageFormat
124+
}
125+
126+
return image
127+
}
128+
129+
// needsVersion returns true if the docker.config.image is making use of the
130+
// ${NOMAD_envoy_version} faux environment variable.
131+
// Nomad does not need to query Consul to get the preferred Envoy version, etc.)
132+
func (h *envoyVersionHook) needsVersion(config map[string]interface{}) bool {
133+
if len(config) == 0 {
134+
return false
135+
}
136+
137+
image := h.taskImage(config)
138+
139+
return strings.Contains(image, structs.EnvoyVersionVar)
140+
}
141+
142+
// image determines the best Envoy version to use. If supported is nil or empty
143+
// Nomad will fallback to the legacy envoy image used before Nomad v0.13.
144+
func (_ *envoyVersionHook) tweakImage(configured string, supported map[string][]string) (string, error) {
145+
versions := supported["envoy"]
146+
if len(versions) == 0 {
147+
return envoyLegacyImage, nil
148+
}
149+
150+
latest, err := semver(versions[0])
151+
if err != nil {
152+
return "", err
153+
}
154+
155+
return strings.ReplaceAll(configured, structs.EnvoyVersionVar, latest), nil
156+
}
157+
158+
// semver sanitizes the envoy version string coming from Consul into the format
159+
// used by the Envoy project when publishing images (i.e. proper semver). This
160+
// resulting string value does NOT contain the 'v' prefix for 2 reasons:
161+
// 1) the version library does not include the 'v'
162+
// 2) its plausible unofficial images use the 3 numbers without the prefix for
163+
// tagging their own images
164+
func semver(chosen string) (string, error) {
165+
v, err := version.NewVersion(chosen)
166+
if err != nil {
167+
return "", errors.Wrap(err, "unexpected envoy version format")
168+
}
169+
return v.String(), nil
170+
}

0 commit comments

Comments
 (0)