1package taskrunner 2 3import ( 4 "context" 5 "testing" 6 7 "github.com/hashicorp/nomad/client/allocdir" 8 ifs "github.com/hashicorp/nomad/client/allocrunner/interfaces" 9 "github.com/hashicorp/nomad/client/taskenv" 10 "github.com/hashicorp/nomad/command/agent/consul" 11 "github.com/hashicorp/nomad/helper/envoy" 12 "github.com/hashicorp/nomad/helper/testlog" 13 "github.com/hashicorp/nomad/nomad/mock" 14 "github.com/hashicorp/nomad/nomad/structs" 15 "github.com/pkg/errors" 16 "github.com/stretchr/testify/require" 17) 18 19var ( 20 taskEnvDefault = taskenv.NewTaskEnv(nil, nil, nil, map[string]string{ 21 "meta.connect.sidecar_image": envoy.ImageFormat, 22 "meta.connect.gateway_image": envoy.ImageFormat, 23 }, "", "") 24) 25 26func TestEnvoyVersionHook_semver(t *testing.T) { 27 t.Parallel() 28 29 t.Run("with v", func(t *testing.T) { 30 result, err := semver("v1.2.3") 31 require.NoError(t, err) 32 require.Equal(t, "1.2.3", result) 33 }) 34 35 t.Run("without v", func(t *testing.T) { 36 result, err := semver("1.2.3") 37 require.NoError(t, err) 38 require.Equal(t, "1.2.3", result) 39 }) 40 41 t.Run("unexpected", func(t *testing.T) { 42 _, err := semver("foo") 43 require.EqualError(t, err, "unexpected envoy version format: Malformed version: foo") 44 }) 45} 46 47func TestEnvoyVersionHook_taskImage(t *testing.T) { 48 t.Parallel() 49 50 t.Run("absent", func(t *testing.T) { 51 result := (*envoyVersionHook)(nil).taskImage(map[string]interface{}{ 52 // empty 53 }) 54 require.Equal(t, envoy.ImageFormat, result) 55 }) 56 57 t.Run("not a string", func(t *testing.T) { 58 result := (*envoyVersionHook)(nil).taskImage(map[string]interface{}{ 59 "image": 7, // not a string 60 }) 61 require.Equal(t, envoy.ImageFormat, result) 62 }) 63 64 t.Run("normal", func(t *testing.T) { 65 result := (*envoyVersionHook)(nil).taskImage(map[string]interface{}{ 66 "image": "custom/envoy:latest", 67 }) 68 require.Equal(t, "custom/envoy:latest", result) 69 }) 70} 71 72func TestEnvoyVersionHook_tweakImage(t *testing.T) { 73 t.Parallel() 74 75 image := envoy.ImageFormat 76 77 t.Run("legacy", func(t *testing.T) { 78 result, err := (*envoyVersionHook)(nil).tweakImage(image, nil) 79 require.NoError(t, err) 80 require.Equal(t, envoy.FallbackImage, result) 81 }) 82 83 t.Run("unexpected", func(t *testing.T) { 84 _, err := (*envoyVersionHook)(nil).tweakImage(image, map[string][]string{ 85 "envoy": {"foo", "bar", "baz"}, 86 }) 87 require.EqualError(t, err, "unexpected envoy version format: Malformed version: foo") 88 }) 89 90 t.Run("standard envoy", func(t *testing.T) { 91 result, err := (*envoyVersionHook)(nil).tweakImage(image, map[string][]string{ 92 "envoy": {"1.15.0", "1.14.4", "1.13.4", "1.12.6"}, 93 }) 94 require.NoError(t, err) 95 require.Equal(t, "envoyproxy/envoy:v1.15.0", result) 96 }) 97 98 t.Run("custom image", func(t *testing.T) { 99 custom := "custom-${NOMAD_envoy_version}/envoy:${NOMAD_envoy_version}" 100 result, err := (*envoyVersionHook)(nil).tweakImage(custom, map[string][]string{ 101 "envoy": {"1.15.0", "1.14.4", "1.13.4", "1.12.6"}, 102 }) 103 require.NoError(t, err) 104 require.Equal(t, "custom-1.15.0/envoy:1.15.0", result) 105 }) 106} 107 108func TestEnvoyVersionHook_interpolateImage(t *testing.T) { 109 t.Parallel() 110 111 hook := (*envoyVersionHook)(nil) 112 113 t.Run("default sidecar", func(t *testing.T) { 114 task := &structs.Task{ 115 Config: map[string]interface{}{"image": envoy.SidecarConfigVar}, 116 } 117 hook.interpolateImage(task, taskEnvDefault) 118 require.Equal(t, envoy.ImageFormat, task.Config["image"]) 119 }) 120 121 t.Run("default gateway", func(t *testing.T) { 122 task := &structs.Task{ 123 Config: map[string]interface{}{"image": envoy.GatewayConfigVar}, 124 } 125 hook.interpolateImage(task, taskEnvDefault) 126 require.Equal(t, envoy.ImageFormat, task.Config["image"]) 127 }) 128 129 t.Run("custom static", func(t *testing.T) { 130 task := &structs.Task{ 131 Config: map[string]interface{}{"image": "custom/envoy"}, 132 } 133 hook.interpolateImage(task, taskEnvDefault) 134 require.Equal(t, "custom/envoy", task.Config["image"]) 135 }) 136 137 t.Run("custom interpolated", func(t *testing.T) { 138 task := &structs.Task{ 139 Config: map[string]interface{}{"image": "${MY_ENVOY}"}, 140 } 141 hook.interpolateImage(task, taskenv.NewTaskEnv(map[string]string{ 142 "MY_ENVOY": "my/envoy", 143 }, map[string]string{ 144 "MY_ENVOY": "my/envoy", 145 }, nil, nil, "", "")) 146 require.Equal(t, "my/envoy", task.Config["image"]) 147 }) 148 149 t.Run("no image", func(t *testing.T) { 150 task := &structs.Task{ 151 Config: map[string]interface{}{}, 152 } 153 hook.interpolateImage(task, taskEnvDefault) 154 require.Empty(t, task.Config) 155 }) 156} 157 158func TestEnvoyVersionHook_skip(t *testing.T) { 159 t.Parallel() 160 161 h := new(envoyVersionHook) 162 163 t.Run("not docker", func(t *testing.T) { 164 skip := h.skip(&ifs.TaskPrestartRequest{ 165 Task: &structs.Task{ 166 Driver: "exec", 167 Config: nil, 168 }, 169 }) 170 require.True(t, skip) 171 }) 172 173 t.Run("not connect", func(t *testing.T) { 174 skip := h.skip(&ifs.TaskPrestartRequest{ 175 Task: &structs.Task{ 176 Driver: "docker", 177 Kind: "", 178 }, 179 }) 180 require.True(t, skip) 181 }) 182 183 t.Run("version not needed", func(t *testing.T) { 184 skip := h.skip(&ifs.TaskPrestartRequest{ 185 Task: &structs.Task{ 186 Driver: "docker", 187 Kind: structs.NewTaskKind(structs.ConnectProxyPrefix, "task"), 188 Config: map[string]interface{}{ 189 "image": "custom/envoy:latest", 190 }, 191 }, 192 }) 193 require.True(t, skip) 194 }) 195 196 t.Run("version needed custom", func(t *testing.T) { 197 skip := h.skip(&ifs.TaskPrestartRequest{ 198 Task: &structs.Task{ 199 Driver: "docker", 200 Kind: structs.NewTaskKind(structs.ConnectProxyPrefix, "task"), 201 Config: map[string]interface{}{ 202 "image": "custom/envoy:v${NOMAD_envoy_version}", 203 }, 204 }, 205 }) 206 require.False(t, skip) 207 }) 208 209 t.Run("version needed standard", func(t *testing.T) { 210 skip := h.skip(&ifs.TaskPrestartRequest{ 211 Task: &structs.Task{ 212 Driver: "docker", 213 Kind: structs.NewTaskKind(structs.ConnectProxyPrefix, "task"), 214 Config: map[string]interface{}{ 215 "image": envoy.ImageFormat, 216 }, 217 }, 218 }) 219 require.False(t, skip) 220 }) 221} 222 223func TestTaskRunner_EnvoyVersionHook_Prestart_standard(t *testing.T) { 224 t.Parallel() 225 226 logger := testlog.HCLogger(t) 227 228 // Setup an Allocation 229 alloc := mock.ConnectAlloc() 230 alloc.Job.TaskGroups[0].Tasks[0] = mock.ConnectSidecarTask() 231 allocDir, cleanupDir := allocdir.TestAllocDir(t, logger, "EnvoyVersionHook") 232 defer cleanupDir() 233 234 // Setup a mock for Consul API 235 spAPI := consul.MockSupportedProxiesAPI{ 236 Value: map[string][]string{ 237 "envoy": {"1.15.0", "1.14.4"}, 238 }, 239 Error: nil, 240 } 241 242 // Run envoy_version hook 243 h := newEnvoyVersionHook(newEnvoyVersionHookConfig(alloc, spAPI, logger)) 244 245 // Create a prestart request 246 request := &ifs.TaskPrestartRequest{ 247 Task: alloc.Job.TaskGroups[0].Tasks[0], 248 TaskDir: allocDir.NewTaskDir(alloc.Job.TaskGroups[0].Tasks[0].Name), 249 TaskEnv: taskEnvDefault, 250 } 251 require.NoError(t, request.TaskDir.Build(false, nil)) 252 253 // Prepare a response 254 var response ifs.TaskPrestartResponse 255 256 // Run the hook 257 require.NoError(t, h.Prestart(context.Background(), request, &response)) 258 259 // Assert the hook is Done 260 require.True(t, response.Done) 261 262 // Assert the Task.Config[image] is concrete 263 require.Equal(t, "envoyproxy/envoy:v1.15.0", request.Task.Config["image"]) 264} 265 266func TestTaskRunner_EnvoyVersionHook_Prestart_custom(t *testing.T) { 267 t.Parallel() 268 269 logger := testlog.HCLogger(t) 270 271 // Setup an Allocation 272 alloc := mock.ConnectAlloc() 273 alloc.Job.TaskGroups[0].Tasks[0] = mock.ConnectSidecarTask() 274 alloc.Job.TaskGroups[0].Tasks[0].Config["image"] = "custom-${NOMAD_envoy_version}:latest" 275 allocDir, cleanupDir := allocdir.TestAllocDir(t, logger, "EnvoyVersionHook") 276 defer cleanupDir() 277 278 // Setup a mock for Consul API 279 spAPI := consul.MockSupportedProxiesAPI{ 280 Value: map[string][]string{ 281 "envoy": {"1.14.1", "1.13.3"}, 282 }, 283 Error: nil, 284 } 285 286 // Run envoy_version hook 287 h := newEnvoyVersionHook(newEnvoyVersionHookConfig(alloc, spAPI, logger)) 288 289 // Create a prestart request 290 request := &ifs.TaskPrestartRequest{ 291 Task: alloc.Job.TaskGroups[0].Tasks[0], 292 TaskDir: allocDir.NewTaskDir(alloc.Job.TaskGroups[0].Tasks[0].Name), 293 TaskEnv: taskEnvDefault, 294 } 295 require.NoError(t, request.TaskDir.Build(false, nil)) 296 297 // Prepare a response 298 var response ifs.TaskPrestartResponse 299 300 // Run the hook 301 require.NoError(t, h.Prestart(context.Background(), request, &response)) 302 303 // Assert the hook is Done 304 require.True(t, response.Done) 305 306 // Assert the Task.Config[image] is concrete 307 require.Equal(t, "custom-1.14.1:latest", request.Task.Config["image"]) 308} 309 310func TestTaskRunner_EnvoyVersionHook_Prestart_skip(t *testing.T) { 311 t.Parallel() 312 313 logger := testlog.HCLogger(t) 314 315 // Setup an Allocation 316 alloc := mock.ConnectAlloc() 317 alloc.Job.TaskGroups[0].Tasks[0] = mock.ConnectSidecarTask() 318 alloc.Job.TaskGroups[0].Tasks[0].Driver = "exec" 319 alloc.Job.TaskGroups[0].Tasks[0].Config = map[string]interface{}{ 320 "command": "/sidecar", 321 } 322 allocDir, cleanupDir := allocdir.TestAllocDir(t, logger, "EnvoyVersionHook") 323 defer cleanupDir() 324 325 // Setup a mock for Consul API 326 spAPI := consul.MockSupportedProxiesAPI{ 327 Value: map[string][]string{ 328 "envoy": {"1.14.1", "1.13.3"}, 329 }, 330 Error: nil, 331 } 332 333 // Run envoy_version hook 334 h := newEnvoyVersionHook(newEnvoyVersionHookConfig(alloc, spAPI, logger)) 335 336 // Create a prestart request 337 request := &ifs.TaskPrestartRequest{ 338 Task: alloc.Job.TaskGroups[0].Tasks[0], 339 TaskDir: allocDir.NewTaskDir(alloc.Job.TaskGroups[0].Tasks[0].Name), 340 TaskEnv: taskEnvDefault, 341 } 342 require.NoError(t, request.TaskDir.Build(false, nil)) 343 344 // Prepare a response 345 var response ifs.TaskPrestartResponse 346 347 // Run the hook 348 require.NoError(t, h.Prestart(context.Background(), request, &response)) 349 350 // Assert the hook is Done 351 require.True(t, response.Done) 352 353 // Assert the Task.Config[image] does not get set 354 require.Empty(t, request.Task.Config["image"]) 355} 356 357func TestTaskRunner_EnvoyVersionHook_Prestart_fallback(t *testing.T) { 358 t.Parallel() 359 360 logger := testlog.HCLogger(t) 361 362 // Setup an Allocation 363 alloc := mock.ConnectAlloc() 364 alloc.Job.TaskGroups[0].Tasks[0] = mock.ConnectSidecarTask() 365 allocDir, cleanupDir := allocdir.TestAllocDir(t, logger, "EnvoyVersionHook") 366 defer cleanupDir() 367 368 // Setup a mock for Consul API 369 spAPI := consul.MockSupportedProxiesAPI{ 370 Value: nil, // old consul, no .xDS.SupportedProxies 371 Error: nil, 372 } 373 374 // Run envoy_version hook 375 h := newEnvoyVersionHook(newEnvoyVersionHookConfig(alloc, spAPI, logger)) 376 377 // Create a prestart request 378 request := &ifs.TaskPrestartRequest{ 379 Task: alloc.Job.TaskGroups[0].Tasks[0], 380 TaskDir: allocDir.NewTaskDir(alloc.Job.TaskGroups[0].Tasks[0].Name), 381 TaskEnv: taskEnvDefault, 382 } 383 require.NoError(t, request.TaskDir.Build(false, nil)) 384 385 // Prepare a response 386 var response ifs.TaskPrestartResponse 387 388 // Run the hook 389 require.NoError(t, h.Prestart(context.Background(), request, &response)) 390 391 // Assert the hook is Done 392 require.True(t, response.Done) 393 394 // Assert the Task.Config[image] is the fallback image 395 require.Equal(t, "envoyproxy/envoy:v1.11.2@sha256:a7769160c9c1a55bb8d07a3b71ce5d64f72b1f665f10d81aa1581bc3cf850d09", request.Task.Config["image"]) 396} 397 398func TestTaskRunner_EnvoyVersionHook_Prestart_error(t *testing.T) { 399 t.Parallel() 400 401 logger := testlog.HCLogger(t) 402 403 // Setup an Allocation 404 alloc := mock.ConnectAlloc() 405 alloc.Job.TaskGroups[0].Tasks[0] = mock.ConnectSidecarTask() 406 allocDir, cleanupDir := allocdir.TestAllocDir(t, logger, "EnvoyVersionHook") 407 defer cleanupDir() 408 409 // Setup a mock for Consul API 410 spAPI := consul.MockSupportedProxiesAPI{ 411 Value: nil, 412 Error: errors.New("some consul error"), 413 } 414 415 // Run envoy_version hook 416 h := newEnvoyVersionHook(newEnvoyVersionHookConfig(alloc, spAPI, logger)) 417 418 // Create a prestart request 419 request := &ifs.TaskPrestartRequest{ 420 Task: alloc.Job.TaskGroups[0].Tasks[0], 421 TaskDir: allocDir.NewTaskDir(alloc.Job.TaskGroups[0].Tasks[0].Name), 422 TaskEnv: taskEnvDefault, 423 } 424 require.NoError(t, request.TaskDir.Build(false, nil)) 425 426 // Prepare a response 427 var response ifs.TaskPrestartResponse 428 429 // Run the hook, error should be recoverable 430 err := h.Prestart(context.Background(), request, &response) 431 require.EqualError(t, err, "error retrieving supported Envoy versions from Consul: some consul error") 432 433 // Assert the hook is not Done 434 require.False(t, response.Done) 435} 436