open-nomad/client/allocrunner/taskrunner/envoy_version_hook.go
Seth Hoenig beaa6359d5 consul/connect: fix regression where client connect images ignored
Nomad v1.0.0 introduced a regression where the client configurations
for `connect.sidecar_image` and `connect.gateway_image` would be
ignored despite being set. This PR restores that functionality.

There was a missing layer of interpolation that needs to occur for
these parameters. Since Nomad 1.0 now supports dynamic envoy versioning
through the ${NOMAD_envoy_version} psuedo variable, we basically need
to first interpolate

  ${connect.sidecar_image} => envoyproxy/envoy:v${NOMAD_envoy_version}

then use Consul at runtime to resolve to a real image, e.g.

  envoyproxy/envoy:v${NOMAD_envoy_version} => envoyproxy/envoy:v1.16.0

Of course, if the version of Consul is too old to provide an envoy
version preference, we then need to know to fallback to the old
version of envoy that we used before.

  envoyproxy/envoy:v${NOMAD_envoy_version} => envoyproxy/envoy:v1.11.2@sha256:a7769160c9c1a55bb8d07a3b71ce5d64f72b1f665f10d81aa1581bc3cf850d09

Beyond that, we also need to continue to support jobs that set the
sidecar task themselves, e.g.

  sidecar_task { config { image: "custom/envoy" } }

which itself could include teh pseudo envoy version variable.
2020-12-14 09:47:55 -06:00

194 lines
6.3 KiB
Go

package taskrunner
import (
"context"
"strings"
"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/client/taskenv"
"github.com/hashicorp/nomad/helper/envoy"
"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"
)
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,
}
}
// envoyVersionHook is used to determine and set the Docker image used for Consul
// Connect sidecar proxy tasks. It will query Consul for a set of preferred Envoy
// versions if the task image is unset or references ${NOMAD_envoy_version}. Nomad
// will fallback the image to the previous default Envoy v1.11.2 if Consul is too old
// to support the supported proxies API.
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(_ context.Context, request *ifs.TaskPrestartRequest, response *ifs.TaskPrestartResponse) error {
// First interpolation of the task image. Typically this turns the default
// ${meta.connect.sidecar_task} into envoyproxy/envoy:v${NOMAD_envoy_version}
// but could be a no-op or some other value if so configured.
h.interpolateImage(request.Task, request.TaskEnv)
// Detect whether this hook needs to run and return early if not. Only run if:
// - task uses docker driver
// - task is a connect sidecar or gateway
// - task image needs ${NOMAD_envoy_version} resolved
if h.skip(request) {
response.Done = true
return nil
}
// We either need to acquire Consul's preferred Envoy version or fallback
// to the legacy default. Query Consul and use the (possibly empty) result.
proxies, err := h.proxiesClient.Proxies()
if err != nil {
return errors.Wrap(err, "error retrieving supported Envoy versions from Consul")
}
// Second [pseudo] interpolation of task image. This determines the concrete
// Envoy image identifier by applying version string substitution of
// ${NOMAD_envoy_version} acquired from Consul.
image, err := h.tweakImage(h.taskImage(request.Task.Config), proxies)
if err != nil {
return errors.Wrap(err, "error interpreting desired Envoy version from Consul")
}
// Set the resulting image.
h.logger.Trace("setting task envoy image", "image", image)
request.Task.Config["image"] = image
response.Done = true
return nil
}
// interpolateImage applies the first pass of interpolation on the task's
// config.image value. This is where ${meta.connect.sidecar_image} or
// ${meta.connect.gateway_image} becomes something that might include the
// ${NOMAD_envoy_version} pseudo variable for further resolution.
func (_ *envoyVersionHook) interpolateImage(task *structs.Task, env *taskenv.TaskEnv) {
value, exists := task.Config["image"]
if !exists {
return
}
image, ok := value.(string)
if !ok {
return
}
task.Config["image"] = env.ReplaceEnv(image)
}
// 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.needsVersion(request.Task.Config):
return true
}
return false
}
// getConfiguredImage extracts the configured config.image value from the request.
// If the image is empty or not a string, Nomad will fallback to the normal
// official Envoy image as if the setting was not configured. This is also what
// Nomad would do if the sidecar_task was not set in the first place.
func (h *envoyVersionHook) taskImage(config map[string]interface{}) string {
value, exists := config["image"]
if !exists {
return envoy.ImageFormat
}
image, ok := value.(string)
if !ok {
return envoy.ImageFormat
}
return image
}
// needsVersion returns true if the docker.config.image is making use of the
// ${NOMAD_envoy_version} faux environment variable, or
// Nomad does not need to query Consul to get the preferred Envoy version, etc.)
func (h *envoyVersionHook) needsVersion(config map[string]interface{}) bool {
if len(config) == 0 {
return false
}
image := h.taskImage(config)
return strings.Contains(image, envoy.VersionVar)
}
// tweakImage 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 (h *envoyVersionHook) tweakImage(configured string, supported map[string][]string) (string, error) {
versions := supported["envoy"]
if len(versions) == 0 {
return envoy.FallbackImage, nil
}
latest, err := semver(versions[0])
if err != nil {
return "", err
}
return strings.ReplaceAll(configured, envoy.VersionVar, 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). This
// resulting string value does NOT contain the 'v' prefix for 2 reasons:
// 1) the version library does not include the 'v'
// 2) its plausible unofficial images use the 3 numbers without the prefix for
// tagging their own images
func semver(chosen string) (string, error) {
v, err := version.NewVersion(chosen)
if err != nil {
return "", errors.Wrap(err, "unexpected envoy version format")
}
return v.String(), nil
}