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