1package taskrunner 2 3import ( 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/client/taskenv" 12 "github.com/hashicorp/nomad/helper/envoy" 13 "github.com/hashicorp/nomad/nomad/structs" 14 "github.com/pkg/errors" 15) 16 17const ( 18 // envoyVersionHookName is the name of this hook and appears in logs. 19 envoyVersionHookName = "envoy_version" 20) 21 22type envoyVersionHookConfig struct { 23 alloc *structs.Allocation 24 proxiesClient consul.SupportedProxiesAPI 25 logger hclog.Logger 26} 27 28func 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. 41type 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 53func newEnvoyVersionHook(c *envoyVersionHookConfig) *envoyVersionHook { 54 return &envoyVersionHook{ 55 alloc: c.alloc, 56 proxiesClient: c.proxiesClient, 57 logger: c.logger.Named(envoyVersionHookName), 58 } 59} 60 61func (envoyVersionHook) Name() string { 62 return envoyVersionHookName 63} 64 65func (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 errors.Wrap(err, "error retrieving supported Envoy versions from Consul") 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 errors.Wrap(err, "error interpreting desired Envoy version from Consul") 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. 106func (_ *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. 122func (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. 138func (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.) 155func (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. 167func (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 187func semver(chosen string) (string, error) { 188 v, err := version.NewVersion(chosen) 189 if err != nil { 190 return "", errors.Wrap(err, "unexpected envoy version format") 191 } 192 return v.String(), nil 193} 194