github.com/adityamillind98/nomad@v0.11.8/nomad/job_endpoint_hook_expose_check_test.go (about) 1 package nomad 2 3 import ( 4 "testing" 5 6 "github.com/hashicorp/nomad/nomad/structs" 7 "github.com/stretchr/testify/require" 8 ) 9 10 func TestJobExposeCheckHook_Name(t *testing.T) { 11 t.Parallel() 12 13 require.Equal(t, "expose-check", new(jobExposeCheckHook).Name()) 14 } 15 16 func TestJobExposeCheckHook_serviceUsesConnectEnvoy(t *testing.T) { 17 t.Parallel() 18 19 t.Run("connect is nil", func(t *testing.T) { 20 require.False(t, serviceUsesConnectEnvoy(&structs.Service{ 21 Connect: nil, 22 })) 23 }) 24 25 t.Run("sidecar-task is overridden", func(t *testing.T) { 26 require.False(t, serviceUsesConnectEnvoy(&structs.Service{ 27 Connect: &structs.ConsulConnect{ 28 SidecarTask: &structs.SidecarTask{ 29 Name: "my-sidecar", 30 }, 31 }, 32 })) 33 }) 34 35 t.Run("sidecar-task is nil", func(t *testing.T) { 36 require.True(t, serviceUsesConnectEnvoy(&structs.Service{ 37 Connect: &structs.ConsulConnect{ 38 SidecarTask: nil, 39 }, 40 })) 41 }) 42 } 43 44 func TestJobExposeCheckHook_tgUsesExposeCheck(t *testing.T) { 45 t.Parallel() 46 47 t.Run("no check.expose", func(t *testing.T) { 48 require.False(t, tgUsesExposeCheck(&structs.TaskGroup{ 49 Services: []*structs.Service{{ 50 Checks: []*structs.ServiceCheck{{ 51 Expose: false, 52 }}, 53 }}, 54 })) 55 }) 56 57 t.Run("with check.expose", func(t *testing.T) { 58 require.True(t, tgUsesExposeCheck(&structs.TaskGroup{ 59 Services: []*structs.Service{{ 60 Checks: []*structs.ServiceCheck{{ 61 Expose: false, 62 }, { 63 Expose: true, 64 }}, 65 }}, 66 })) 67 }) 68 } 69 70 func TestJobExposeCheckHook_tgValidateUseOfBridgeMode(t *testing.T) { 71 t.Parallel() 72 73 s1 := &structs.Service{ 74 Name: "s1", 75 Checks: []*structs.ServiceCheck{{ 76 Name: "s1-check1", 77 Type: "http", 78 PortLabel: "health", 79 Expose: true, 80 }}, 81 } 82 83 t.Run("no networks but no use of expose", func(t *testing.T) { 84 require.Nil(t, tgValidateUseOfBridgeMode(&structs.TaskGroup{ 85 Networks: make(structs.Networks, 0), 86 })) 87 }) 88 89 t.Run("no networks and uses expose", func(t *testing.T) { 90 require.EqualError(t, tgValidateUseOfBridgeMode(&structs.TaskGroup{ 91 Name: "g1", 92 Networks: make(structs.Networks, 0), 93 Services: []*structs.Service{s1}, 94 }), `group "g1" must specify one bridge network for exposing service check(s)`) 95 }) 96 97 t.Run("non-bridge network and uses expose", func(t *testing.T) { 98 require.EqualError(t, tgValidateUseOfBridgeMode(&structs.TaskGroup{ 99 Name: "g1", 100 Networks: structs.Networks{{ 101 Mode: "host", 102 }}, 103 Services: []*structs.Service{s1}, 104 }), `group "g1" must use bridge network for exposing service check(s)`) 105 }) 106 107 t.Run("bridge network uses expose", func(t *testing.T) { 108 require.Nil(t, tgValidateUseOfBridgeMode(&structs.TaskGroup{ 109 Name: "g1", 110 Networks: structs.Networks{{ 111 Mode: "bridge", 112 }}, 113 Services: []*structs.Service{s1}, 114 })) 115 }) 116 } 117 118 func TestJobExposeCheckHook_tgValidateUseOfCheckExpose(t *testing.T) { 119 t.Parallel() 120 121 withCustomProxyTask := &structs.Service{ 122 Name: "s1", 123 Connect: &structs.ConsulConnect{ 124 SidecarTask: &structs.SidecarTask{Name: "custom"}, 125 }, 126 Checks: []*structs.ServiceCheck{{ 127 Name: "s1-check1", 128 Type: "http", 129 PortLabel: "health", 130 Expose: true, 131 }}, 132 } 133 134 t.Run("group-service uses custom proxy", func(t *testing.T) { 135 require.EqualError(t, tgValidateUseOfCheckExpose(&structs.TaskGroup{ 136 Name: "g1", 137 Services: []*structs.Service{withCustomProxyTask}, 138 }), `exposed service check g1->s1->s1-check1 requires use of Nomad's builtin Connect proxy`) 139 }) 140 141 t.Run("group-service uses custom proxy but no expose", func(t *testing.T) { 142 withCustomProxyTaskNoExpose := &(*withCustomProxyTask) 143 withCustomProxyTask.Checks[0].Expose = false 144 require.Nil(t, tgValidateUseOfCheckExpose(&structs.TaskGroup{ 145 Name: "g1", 146 Services: []*structs.Service{withCustomProxyTaskNoExpose}, 147 })) 148 }) 149 150 t.Run("task-service sets expose", func(t *testing.T) { 151 require.EqualError(t, tgValidateUseOfCheckExpose(&structs.TaskGroup{ 152 Name: "g1", 153 Tasks: []*structs.Task{{ 154 Name: "t1", 155 Services: []*structs.Service{{ 156 Name: "s2", 157 Checks: []*structs.ServiceCheck{{ 158 Name: "check1", 159 Type: "http", 160 Expose: true, 161 }}, 162 }}, 163 }}, 164 }), `exposed service check g1[t1]->s2->check1 is not a task-group service`) 165 }) 166 } 167 168 func TestJobExposeCheckHook_Validate(t *testing.T) { 169 s1 := &structs.Service{ 170 Name: "s1", 171 Checks: []*structs.ServiceCheck{{ 172 Name: "s1-check1", 173 Type: "http", 174 Expose: true, 175 }}, 176 } 177 178 t.Run("double network", func(t *testing.T) { 179 warnings, err := new(jobExposeCheckHook).Validate(&structs.Job{ 180 TaskGroups: []*structs.TaskGroup{{ 181 Name: "g1", 182 Networks: structs.Networks{{ 183 Mode: "bridge", 184 }, { 185 Mode: "bridge", 186 }}, 187 Services: []*structs.Service{s1}, 188 }}, 189 }) 190 require.Empty(t, warnings) 191 require.EqualError(t, err, `group "g1" must specify one bridge network for exposing service check(s)`) 192 }) 193 194 t.Run("expose in service check", func(t *testing.T) { 195 warnings, err := new(jobExposeCheckHook).Validate(&structs.Job{ 196 TaskGroups: []*structs.TaskGroup{{ 197 Name: "g1", 198 Networks: structs.Networks{{ 199 Mode: "bridge", 200 }}, 201 Tasks: []*structs.Task{{ 202 Name: "t1", 203 Services: []*structs.Service{{ 204 Name: "s2", 205 Checks: []*structs.ServiceCheck{{ 206 Name: "s2-check1", 207 Type: "http", 208 Expose: true, 209 }}, 210 }}, 211 }}, 212 }}, 213 }) 214 require.Empty(t, warnings) 215 require.EqualError(t, err, `exposed service check g1[t1]->s2->s2-check1 is not a task-group service`) 216 }) 217 218 t.Run("ok", func(t *testing.T) { 219 warnings, err := new(jobExposeCheckHook).Validate(&structs.Job{ 220 TaskGroups: []*structs.TaskGroup{{ 221 Name: "g1", 222 Networks: structs.Networks{{ 223 Mode: "bridge", 224 }}, 225 Services: []*structs.Service{{ 226 Name: "s1", 227 Connect: &structs.ConsulConnect{}, 228 Checks: []*structs.ServiceCheck{{ 229 Name: "check1", 230 Type: "http", 231 Expose: true, 232 }}, 233 }}, 234 Tasks: []*structs.Task{{ 235 Name: "t1", 236 Services: []*structs.Service{{ 237 Name: "s2", 238 Checks: []*structs.ServiceCheck{{ 239 Name: "s2-check1", 240 Type: "http", 241 Expose: false, 242 }}, 243 }}, 244 }}, 245 }}, 246 }) 247 require.Empty(t, warnings) 248 require.Nil(t, err) 249 }) 250 } 251 252 func TestJobExposeCheckHook_exposePathForCheck(t *testing.T) { 253 t.Parallel() 254 255 t.Run("not expose compatible", func(t *testing.T) { 256 c := &structs.ServiceCheck{ 257 Type: "tcp", // not expose compatible 258 } 259 s := &structs.Service{ 260 Checks: []*structs.ServiceCheck{c}, 261 } 262 ePath, err := exposePathForCheck(&structs.TaskGroup{ 263 Services: []*structs.Service{s}, 264 }, s, c) 265 require.NoError(t, err) 266 require.Nil(t, ePath) 267 }) 268 269 t.Run("direct port", func(t *testing.T) { 270 c := &structs.ServiceCheck{ 271 Name: "check1", 272 Type: "http", 273 Path: "/health", 274 PortLabel: "hcPort", 275 } 276 s := &structs.Service{ 277 Name: "service1", 278 PortLabel: "4000", 279 Checks: []*structs.ServiceCheck{c}, 280 } 281 ePath, err := exposePathForCheck(&structs.TaskGroup{ 282 Name: "group1", 283 Services: []*structs.Service{s}, 284 }, s, c) 285 require.NoError(t, err) 286 require.Equal(t, &structs.ConsulExposePath{ 287 Path: "/health", 288 Protocol: "", // often blank, consul does the Right Thing 289 LocalPathPort: 4000, 290 ListenerPort: "hcPort", 291 }, ePath) 292 }) 293 294 t.Run("labeled port", func(t *testing.T) { 295 c := &structs.ServiceCheck{ 296 Name: "check1", 297 Type: "http", 298 Path: "/health", 299 PortLabel: "hcPort", 300 } 301 s := &structs.Service{ 302 Name: "service1", 303 PortLabel: "sPort", // port label indirection 304 Checks: []*structs.ServiceCheck{c}, 305 } 306 ePath, err := exposePathForCheck(&structs.TaskGroup{ 307 Name: "group1", 308 Services: []*structs.Service{s}, 309 Networks: structs.Networks{{ 310 Mode: "bridge", 311 DynamicPorts: []structs.Port{ 312 {Label: "sPort", Value: 4000}, 313 }, 314 }}, 315 }, s, c) 316 require.NoError(t, err) 317 require.Equal(t, &structs.ConsulExposePath{ 318 Path: "/health", 319 Protocol: "", 320 LocalPathPort: 4000, 321 ListenerPort: "hcPort", 322 }, ePath) 323 }) 324 325 t.Run("missing port", func(t *testing.T) { 326 c := &structs.ServiceCheck{ 327 Name: "check1", 328 Type: "http", 329 Path: "/health", 330 PortLabel: "hcPort", 331 } 332 s := &structs.Service{ 333 Name: "service1", 334 PortLabel: "sPort", // port label indirection 335 Checks: []*structs.ServiceCheck{c}, 336 } 337 _, err := exposePathForCheck(&structs.TaskGroup{ 338 Name: "group1", 339 Services: []*structs.Service{s}, 340 Networks: structs.Networks{{ 341 Mode: "bridge", 342 DynamicPorts: []structs.Port{ 343 // service declares "sPort", but does not exist 344 }, 345 }}, 346 }, s, c) 347 require.EqualError(t, err, `unable to determine local service port for service check group1->service1->check1`) 348 }) 349 350 t.Run("empty check port", func(t *testing.T) { 351 c := &structs.ServiceCheck{ 352 Name: "check1", 353 Type: "http", 354 Path: "/health", 355 } 356 s := &structs.Service{ 357 Name: "service1", 358 PortLabel: "9999", 359 Checks: []*structs.ServiceCheck{c}, 360 } 361 tg := &structs.TaskGroup{ 362 Name: "group1", 363 Services: []*structs.Service{s}, 364 Networks: structs.Networks{{ 365 Mode: "bridge", 366 DynamicPorts: []structs.Port{}, 367 }}, 368 } 369 ePath, err := exposePathForCheck(tg, s, c) 370 require.NoError(t, err) 371 require.Len(t, tg.Networks[0].DynamicPorts, 1) 372 require.Equal(t, &structs.ConsulExposePath{ 373 Path: "/health", 374 Protocol: "", 375 LocalPathPort: 9999, 376 ListenerPort: tg.Networks[0].DynamicPorts[0].Label, 377 }, ePath) 378 }) 379 } 380 381 func TestJobExposeCheckHook_containsExposePath(t *testing.T) { 382 t.Parallel() 383 384 t.Run("contains path", func(t *testing.T) { 385 require.True(t, containsExposePath([]structs.ConsulExposePath{{ 386 Path: "/v2/health", 387 Protocol: "grpc", 388 LocalPathPort: 8080, 389 ListenerPort: "v2Port", 390 }, { 391 Path: "/health", 392 Protocol: "http", 393 LocalPathPort: 8080, 394 ListenerPort: "hcPort", 395 }}, structs.ConsulExposePath{ 396 Path: "/health", 397 Protocol: "http", 398 LocalPathPort: 8080, 399 ListenerPort: "hcPort", 400 })) 401 }) 402 403 t.Run("no such path", func(t *testing.T) { 404 require.False(t, containsExposePath([]structs.ConsulExposePath{{ 405 Path: "/v2/health", 406 Protocol: "grpc", 407 LocalPathPort: 8080, 408 ListenerPort: "v2Port", 409 }, { 410 Path: "/health", 411 Protocol: "http", 412 LocalPathPort: 8080, 413 ListenerPort: "hcPort", 414 }}, structs.ConsulExposePath{ 415 Path: "/v3/health", 416 Protocol: "http", 417 LocalPathPort: 8080, 418 ListenerPort: "hcPort", 419 })) 420 }) 421 } 422 423 func TestJobExposeCheckHook_serviceExposeConfig(t *testing.T) { 424 t.Parallel() 425 426 t.Run("proxy is nil", func(t *testing.T) { 427 require.NotNil(t, serviceExposeConfig(&structs.Service{ 428 Connect: &structs.ConsulConnect{ 429 SidecarService: &structs.ConsulSidecarService{}, 430 }, 431 })) 432 }) 433 434 t.Run("expose is nil", func(t *testing.T) { 435 require.NotNil(t, serviceExposeConfig(&structs.Service{ 436 Connect: &structs.ConsulConnect{ 437 SidecarService: &structs.ConsulSidecarService{ 438 Proxy: &structs.ConsulProxy{}, 439 }, 440 }, 441 })) 442 }) 443 444 t.Run("expose pre-existing", func(t *testing.T) { 445 exposeConfig := serviceExposeConfig(&structs.Service{ 446 Connect: &structs.ConsulConnect{ 447 SidecarService: &structs.ConsulSidecarService{ 448 Proxy: &structs.ConsulProxy{ 449 Expose: &structs.ConsulExposeConfig{ 450 Paths: []structs.ConsulExposePath{{ 451 Path: "/health", 452 }}, 453 }, 454 }, 455 }, 456 }, 457 }) 458 require.NotNil(t, exposeConfig) 459 require.Equal(t, []structs.ConsulExposePath{{ 460 Path: "/health", 461 }}, exposeConfig.Paths) 462 }) 463 464 t.Run("append to paths is safe", func(t *testing.T) { 465 // double check that serviceExposeConfig(s).Paths can be appended to 466 // from a derived pointer without fear of the original underlying array 467 // pointer being lost 468 469 s := &structs.Service{ 470 Connect: &structs.ConsulConnect{ 471 SidecarService: &structs.ConsulSidecarService{ 472 Proxy: &structs.ConsulProxy{ 473 Expose: &structs.ConsulExposeConfig{ 474 Paths: []structs.ConsulExposePath{{ 475 Path: "/one", 476 }}, 477 }, 478 }, 479 }, 480 }, 481 } 482 483 exposeConfig := serviceExposeConfig(s) 484 exposeConfig.Paths = append(exposeConfig.Paths, 485 structs.ConsulExposePath{Path: "/two"}, 486 structs.ConsulExposePath{Path: "/three"}, 487 structs.ConsulExposePath{Path: "/four"}, 488 structs.ConsulExposePath{Path: "/five"}, 489 structs.ConsulExposePath{Path: "/six"}, 490 structs.ConsulExposePath{Path: "/seven"}, 491 structs.ConsulExposePath{Path: "/eight"}, 492 structs.ConsulExposePath{Path: "/nine"}, 493 ) 494 495 // works, because exposeConfig.Paths gets re-assigned into exposeConfig 496 // which is a pointer, meaning the field is modified also from the 497 // service struct's perspective 498 require.Equal(t, 9, len(s.Connect.SidecarService.Proxy.Expose.Paths)) 499 }) 500 } 501 502 func TestJobExposeCheckHook_checkIsExposable(t *testing.T) { 503 t.Parallel() 504 505 t.Run("grpc", func(t *testing.T) { 506 require.True(t, checkIsExposable(&structs.ServiceCheck{ 507 Type: "grpc", 508 Path: "/health", 509 })) 510 require.True(t, checkIsExposable(&structs.ServiceCheck{ 511 Type: "gRPC", 512 Path: "/health", 513 })) 514 }) 515 516 t.Run("http", func(t *testing.T) { 517 require.True(t, checkIsExposable(&structs.ServiceCheck{ 518 Type: "http", 519 Path: "/health", 520 })) 521 require.True(t, checkIsExposable(&structs.ServiceCheck{ 522 Type: "HTTP", 523 Path: "/health", 524 })) 525 }) 526 527 t.Run("tcp", func(t *testing.T) { 528 require.False(t, checkIsExposable(&structs.ServiceCheck{ 529 Type: "tcp", 530 Path: "/health", 531 })) 532 }) 533 534 t.Run("no path slash prefix", func(t *testing.T) { 535 require.False(t, checkIsExposable(&structs.ServiceCheck{ 536 Type: "http", 537 Path: "health", 538 })) 539 }) 540 } 541 542 func TestJobExposeCheckHook_Mutate(t *testing.T) { 543 t.Parallel() 544 545 t.Run("typical", func(t *testing.T) { 546 result, warnings, err := new(jobExposeCheckHook).Mutate(&structs.Job{ 547 TaskGroups: []*structs.TaskGroup{{ 548 Name: "group0", 549 Networks: structs.Networks{{ 550 Mode: "host", 551 }}, 552 }, { 553 Name: "group1", 554 Networks: structs.Networks{{ 555 Mode: "bridge", 556 }}, 557 Services: []*structs.Service{{ 558 Name: "service1", 559 PortLabel: "8000", 560 Checks: []*structs.ServiceCheck{{ 561 Name: "check1", 562 Type: "tcp", 563 PortLabel: "8100", 564 }, { 565 Name: "check2", 566 Type: "http", 567 PortLabel: "health", 568 Path: "/health", 569 Expose: true, 570 }, { 571 Name: "check3", 572 Type: "grpc", 573 PortLabel: "health", 574 Path: "/v2/health", 575 Expose: true, 576 }}, 577 Connect: &structs.ConsulConnect{ 578 SidecarService: &structs.ConsulSidecarService{ 579 Proxy: &structs.ConsulProxy{ 580 Expose: &structs.ConsulExposeConfig{ 581 Paths: []structs.ConsulExposePath{{ 582 Path: "/pre-existing", 583 Protocol: "http", 584 LocalPathPort: 9000, 585 ListenerPort: "otherPort", 586 }}}}}}}, { 587 Name: "service2", 588 PortLabel: "3000", 589 Checks: []*structs.ServiceCheck{{ 590 Name: "check1", 591 Type: "grpc", 592 Protocol: "http2", 593 Path: "/ok", 594 PortLabel: "health", 595 Expose: true, 596 }}, 597 Connect: &structs.ConsulConnect{ 598 SidecarService: &structs.ConsulSidecarService{ 599 Proxy: &structs.ConsulProxy{}, 600 }, 601 }, 602 }}}}, 603 }) 604 605 require.NoError(t, err) 606 require.Empty(t, warnings) 607 require.Equal(t, []structs.ConsulExposePath{{ 608 Path: "/pre-existing", 609 LocalPathPort: 9000, 610 Protocol: "http", 611 ListenerPort: "otherPort", 612 }, { 613 Path: "/health", 614 LocalPathPort: 8000, 615 ListenerPort: "health", 616 }, { 617 Path: "/v2/health", 618 LocalPathPort: 8000, 619 ListenerPort: "health", 620 }}, result.TaskGroups[1].Services[0].Connect.SidecarService.Proxy.Expose.Paths) 621 require.Equal(t, []structs.ConsulExposePath{{ 622 Path: "/ok", 623 LocalPathPort: 3000, 624 Protocol: "http2", 625 ListenerPort: "health", 626 }}, result.TaskGroups[1].Services[1].Connect.SidecarService.Proxy.Expose.Paths) 627 }) 628 }