2020-09-04 17:50:11 +00:00
|
|
|
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/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.
|
|
|
|
//
|
2020-10-14 22:17:47 +00:00
|
|
|
// This is the version defaulted by Nomad before v1.0 and/or when using versions
|
2020-09-04 17:50:11 +00:00
|
|
|
// of Consul before v1.7.8, v1.8.5, and v1.9.0.
|
|
|
|
envoyLegacyImage = "envoyproxy/envoy:v1.11.2@sha256:a7769160c9c1a55bb8d07a3b71ce5d64f72b1f665f10d81aa1581bc3cf850d09"
|
|
|
|
)
|
|
|
|
|
|
|
|
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 {
|
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Determine the concrete Envoy image identifier by applying version string
|
|
|
|
// substitution (${NOMAD_envoy_version}).
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 (_ *envoyVersionHook) taskImage(config map[string]interface{}) string {
|
|
|
|
value, exists := config["image"]
|
|
|
|
if !exists {
|
|
|
|
return structs.EnvoyImageFormat
|
|
|
|
}
|
|
|
|
|
|
|
|
image, ok := value.(string)
|
|
|
|
if !ok {
|
|
|
|
return structs.EnvoyImageFormat
|
|
|
|
}
|
|
|
|
|
|
|
|
return image
|
|
|
|
}
|
|
|
|
|
|
|
|
// needsVersion returns true if the docker.config.image is making use of the
|
|
|
|
// ${NOMAD_envoy_version} faux environment variable.
|
|
|
|
// 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, structs.EnvoyVersionVar)
|
|
|
|
}
|
|
|
|
|
|
|
|
// image determines the best Envoy version to use. If supported is nil or empty
|
2020-10-14 22:17:47 +00:00
|
|
|
// Nomad will fallback to the legacy envoy image used before Nomad v1.0.
|
2020-09-04 17:50:11 +00:00
|
|
|
func (_ *envoyVersionHook) tweakImage(configured string, 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 strings.ReplaceAll(configured, structs.EnvoyVersionVar, 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
|
|
|
|
}
|