github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/client/allocrunner/taskrunner/envoy_version_hook.go (about)

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