github.com/hernad/nomad@v1.6.112/nomad/structs/services_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package structs 5 6 import ( 7 "errors" 8 "testing" 9 "time" 10 11 "github.com/hashicorp/go-multierror" 12 "github.com/hernad/nomad/ci" 13 "github.com/hernad/nomad/helper/pointer" 14 "github.com/shoenig/test/must" 15 "github.com/stretchr/testify/require" 16 ) 17 18 func TestServiceCheck_Hash(t *testing.T) { 19 ci.Parallel(t) 20 21 original := &ServiceCheck{ 22 Name: "check", 23 SuccessBeforePassing: 3, 24 FailuresBeforeCritical: 4, 25 } 26 27 type sc = ServiceCheck 28 type tweaker = func(check *sc) 29 30 hash := func(c *sc) string { 31 return c.Hash("ServiceID") 32 } 33 34 t.Run("reflexive", func(t *testing.T) { 35 require.Equal(t, hash(original), hash(original)) 36 }) 37 38 // these tests use tweaker to modify 1 field and make the false assertion 39 // on comparing the resulting hash output 40 41 try := func(t *testing.T, tweak tweaker) { 42 originalHash := hash(original) 43 modifiable := original.Copy() 44 tweak(modifiable) 45 tweakedHash := hash(modifiable) 46 require.NotEqual(t, originalHash, tweakedHash) 47 } 48 49 t.Run("name", func(t *testing.T) { 50 try(t, func(s *sc) { s.Name = "newName" }) 51 }) 52 53 t.Run("success_before_passing", func(t *testing.T) { 54 try(t, func(s *sc) { s.SuccessBeforePassing = 99 }) 55 }) 56 57 t.Run("failures_before_critical", func(t *testing.T) { 58 try(t, func(s *sc) { s.FailuresBeforeCritical = 99 }) 59 }) 60 } 61 62 func TestServiceCheck_Canonicalize(t *testing.T) { 63 ci.Parallel(t) 64 65 t.Run("defaults", func(t *testing.T) { 66 sc := &ServiceCheck{ 67 Args: []string{}, 68 Header: make(map[string][]string), 69 Method: "", 70 OnUpdate: "", 71 } 72 sc.Canonicalize("MyService", "task1") 73 must.Nil(t, sc.Args) 74 must.Nil(t, sc.Header) 75 must.Eq(t, `service: "MyService" check`, sc.Name) 76 must.Eq(t, "", sc.Method) 77 must.Eq(t, OnUpdateRequireHealthy, sc.OnUpdate) 78 }) 79 80 t.Run("check name set", func(t *testing.T) { 81 sc := &ServiceCheck{ 82 Name: "Some Check", 83 } 84 sc.Canonicalize("MyService", "task1") 85 must.Eq(t, "Some Check", sc.Name) 86 }) 87 88 t.Run("on_update is set", func(t *testing.T) { 89 sc := &ServiceCheck{ 90 OnUpdate: OnUpdateIgnore, 91 } 92 sc.Canonicalize("MyService", "task1") 93 must.Eq(t, OnUpdateIgnore, sc.OnUpdate) 94 }) 95 } 96 97 func TestServiceCheck_validate_PassingTypes(t *testing.T) { 98 ci.Parallel(t) 99 100 t.Run("valid", func(t *testing.T) { 101 for _, checkType := range []string{"tcp", "http", "grpc"} { 102 err := (&ServiceCheck{ 103 Name: "check", 104 Type: checkType, 105 Path: "/path", 106 Interval: 1 * time.Second, 107 Timeout: 2 * time.Second, 108 SuccessBeforePassing: 3, 109 }).validateConsul() 110 require.NoError(t, err) 111 } 112 }) 113 114 t.Run("invalid", func(t *testing.T) { 115 err := (&ServiceCheck{ 116 Name: "check", 117 Type: "script", 118 Command: "/nothing", 119 Interval: 1 * time.Second, 120 Timeout: 2 * time.Second, 121 SuccessBeforePassing: 3, 122 }).validateConsul() 123 require.EqualError(t, err, `success_before_passing not supported for check of type "script"`) 124 }) 125 } 126 127 func TestServiceCheck_validate_FailingTypes(t *testing.T) { 128 ci.Parallel(t) 129 130 t.Run("valid", func(t *testing.T) { 131 for _, checkType := range []string{"tcp", "http", "grpc"} { 132 err := (&ServiceCheck{ 133 Name: "check", 134 Type: checkType, 135 Path: "/path", 136 Interval: 1 * time.Second, 137 Timeout: 2 * time.Second, 138 FailuresBeforeCritical: 3, 139 }).validateConsul() 140 require.NoError(t, err) 141 } 142 }) 143 144 t.Run("invalid", func(t *testing.T) { 145 err := (&ServiceCheck{ 146 Name: "check", 147 Type: "script", 148 Command: "/nothing", 149 Interval: 1 * time.Second, 150 Timeout: 2 * time.Second, 151 SuccessBeforePassing: 0, 152 FailuresBeforeCritical: 3, 153 }).validateConsul() 154 require.EqualError(t, err, `failures_before_critical not supported for check of type "script"`) 155 }) 156 } 157 158 func TestServiceCheck_validate_PassFailZero_on_scripts(t *testing.T) { 159 ci.Parallel(t) 160 161 t.Run("invalid", func(t *testing.T) { 162 err := (&ServiceCheck{ 163 Name: "check", 164 Type: "script", 165 Command: "/nothing", 166 Interval: 1 * time.Second, 167 Timeout: 2 * time.Second, 168 SuccessBeforePassing: 0, // script checks should still pass validation 169 FailuresBeforeCritical: 0, // script checks should still pass validation 170 }).validateConsul() 171 require.NoError(t, err) 172 }) 173 } 174 175 func TestServiceCheck_validate_OnUpdate_CheckRestart_Conflict(t *testing.T) { 176 ci.Parallel(t) 177 178 t.Run("invalid", func(t *testing.T) { 179 err := (&ServiceCheck{ 180 Name: "check", 181 Type: "script", 182 Command: "/nothing", 183 Interval: 1 * time.Second, 184 Timeout: 2 * time.Second, 185 CheckRestart: &CheckRestart{ 186 IgnoreWarnings: false, 187 Limit: 3, 188 Grace: 5 * time.Second, 189 }, 190 OnUpdate: OnUpdateIgnoreWarn, 191 }).validateConsul() 192 require.EqualError(t, err, `on_update value "ignore_warnings" not supported with check_restart ignore_warnings value "false"`) 193 }) 194 195 t.Run("invalid", func(t *testing.T) { 196 err := (&ServiceCheck{ 197 Name: "check", 198 Type: "script", 199 Command: "/nothing", 200 Interval: 1 * time.Second, 201 Timeout: 2 * time.Second, 202 CheckRestart: &CheckRestart{ 203 IgnoreWarnings: false, 204 Limit: 3, 205 Grace: 5 * time.Second, 206 }, 207 OnUpdate: OnUpdateIgnore, 208 }).validateConsul() 209 require.EqualError(t, err, `on_update value "ignore" is not compatible with check_restart`) 210 }) 211 212 t.Run("valid", func(t *testing.T) { 213 err := (&ServiceCheck{ 214 Name: "check", 215 Type: "script", 216 Command: "/nothing", 217 Interval: 1 * time.Second, 218 Timeout: 2 * time.Second, 219 CheckRestart: &CheckRestart{ 220 IgnoreWarnings: true, 221 Limit: 3, 222 Grace: 5 * time.Second, 223 }, 224 OnUpdate: OnUpdateIgnoreWarn, 225 }).validateConsul() 226 require.NoError(t, err) 227 }) 228 } 229 230 func TestServiceCheck_validateNomad(t *testing.T) { 231 ci.Parallel(t) 232 233 testCases := []struct { 234 name string 235 sc *ServiceCheck 236 exp string 237 }{ 238 {name: "grpc", sc: &ServiceCheck{Type: ServiceCheckGRPC}, exp: `invalid check type ("grpc"), must be one of tcp, http`}, 239 {name: "script", sc: &ServiceCheck{Type: ServiceCheckScript}, exp: `invalid check type ("script"), must be one of tcp, http`}, 240 { 241 name: "expose", 242 sc: &ServiceCheck{ 243 Type: ServiceCheckTCP, 244 Expose: true, // consul only 245 Interval: 3 * time.Second, 246 Timeout: 1 * time.Second, 247 }, 248 exp: `expose may only be set for Consul service checks`, 249 }, { 250 name: "on_update ignore_warnings", 251 sc: &ServiceCheck{ 252 Type: ServiceCheckTCP, 253 Interval: 3 * time.Second, 254 Timeout: 1 * time.Second, 255 OnUpdate: OnUpdateIgnoreWarn, 256 }, 257 exp: `on_update may only be set to ignore_warnings for Consul service checks`, 258 }, 259 { 260 name: "success_before_passing", 261 sc: &ServiceCheck{ 262 Type: ServiceCheckTCP, 263 SuccessBeforePassing: 3, // consul only 264 Interval: 3 * time.Second, 265 Timeout: 1 * time.Second, 266 }, 267 exp: `success_before_passing may only be set for Consul service checks`, 268 }, 269 { 270 name: "failures_before_critical", 271 sc: &ServiceCheck{ 272 Type: ServiceCheckTCP, 273 FailuresBeforeCritical: 3, // consul only 274 Interval: 3 * time.Second, 275 Timeout: 1 * time.Second, 276 }, 277 exp: `failures_before_critical may only be set for Consul service checks`, 278 }, 279 { 280 name: "check_restart", 281 sc: &ServiceCheck{ 282 Type: ServiceCheckTCP, 283 Interval: 3 * time.Second, 284 Timeout: 1 * time.Second, 285 CheckRestart: new(CheckRestart), 286 }, 287 }, 288 { 289 name: "check_restart ignore_warnings", 290 sc: &ServiceCheck{ 291 Type: ServiceCheckTCP, 292 Interval: 3 * time.Second, 293 Timeout: 1 * time.Second, 294 CheckRestart: &CheckRestart{ 295 IgnoreWarnings: true, 296 }, 297 }, 298 exp: `ignore_warnings on check_restart only supported for Consul service checks`, 299 }, 300 { 301 name: "address mode driver", 302 sc: &ServiceCheck{ 303 Type: ServiceCheckTCP, 304 Interval: 3 * time.Second, 305 Timeout: 1 * time.Second, 306 AddressMode: "driver", 307 }, 308 exp: `address_mode = driver may only be set for Consul service checks`, 309 }, 310 { 311 name: "http non GET", 312 sc: &ServiceCheck{ 313 Type: ServiceCheckHTTP, 314 Interval: 3 * time.Second, 315 Timeout: 1 * time.Second, 316 Path: "/health", 317 Method: "HEAD", 318 }, 319 }, 320 { 321 name: "http unknown method type", 322 sc: &ServiceCheck{ 323 Type: ServiceCheckHTTP, 324 Interval: 3 * time.Second, 325 Timeout: 1 * time.Second, 326 Path: "/health", 327 Method: "Invalid", 328 }, 329 exp: `method type "Invalid" not supported in Nomad http check`, 330 }, 331 { 332 name: "http with headers", 333 sc: &ServiceCheck{ 334 Type: ServiceCheckHTTP, 335 Interval: 3 * time.Second, 336 Timeout: 1 * time.Second, 337 Path: "/health", 338 Method: "GET", 339 Header: map[string][]string{ 340 "foo": {"bar"}, 341 "baz": nil, 342 }, 343 }, 344 }, 345 { 346 name: "http with body", 347 sc: &ServiceCheck{ 348 Type: ServiceCheckHTTP, 349 Interval: 3 * time.Second, 350 Timeout: 1 * time.Second, 351 Path: "/health", 352 Method: "POST", 353 Body: "this is a request payload!", 354 }, 355 }, 356 { 357 name: "http with tls_server_name", 358 sc: &ServiceCheck{ 359 Type: ServiceCheckHTTP, 360 Interval: 3 * time.Second, 361 Timeout: 1 * time.Second, 362 Path: "/health", 363 TLSServerName: "foo", 364 }, 365 exp: `tls_server_name may only be set for Consul service checks`, 366 }, 367 } 368 369 for _, testCase := range testCases { 370 t.Run(testCase.name, func(t *testing.T) { 371 err := testCase.sc.validateNomad() 372 if testCase.exp == "" { 373 must.NoError(t, err) 374 } else { 375 must.EqError(t, err, testCase.exp) 376 } 377 }) 378 } 379 } 380 381 func TestService_Hash(t *testing.T) { 382 ci.Parallel(t) 383 384 original := &Service{ 385 Name: "myService", 386 PortLabel: "portLabel", 387 // AddressMode: "bridge", // not hashed (used internally by Nomad) 388 Tags: []string{"original", "tags"}, 389 CanaryTags: []string{"canary", "tags"}, 390 // Checks: nil, // not hashed (managed independently) 391 Connect: &ConsulConnect{ 392 // Native: false, // not hashed 393 SidecarService: &ConsulSidecarService{ 394 Tags: []string{"original", "sidecar", "tags"}, 395 Port: "9000", 396 Proxy: &ConsulProxy{ 397 LocalServiceAddress: "127.0.0.1", 398 LocalServicePort: 24000, 399 Config: map[string]any{"foo": "bar"}, 400 Upstreams: []ConsulUpstream{{ 401 DestinationName: "upstream1", 402 DestinationNamespace: "ns2", 403 LocalBindPort: 29000, 404 Config: map[string]any{"foo": "bar"}, 405 }}, 406 }, 407 Meta: map[string]string{ 408 "test-key": "test-value", 409 }, 410 }, 411 // SidecarTask: nil // not hashed 412 }} 413 414 type svc = Service 415 type tweaker = func(service *svc) 416 417 hash := func(s *svc, canary bool) string { 418 return s.Hash("AllocID", "TaskName", canary) 419 } 420 421 t.Run("matching and is canary", func(t *testing.T) { 422 require.Equal(t, hash(original, true), hash(original, true)) 423 }) 424 425 t.Run("matching and is not canary", func(t *testing.T) { 426 require.Equal(t, hash(original, false), hash(original, false)) 427 }) 428 429 t.Run("matching mod canary", func(t *testing.T) { 430 require.NotEqual(t, hash(original, true), hash(original, false)) 431 }) 432 433 try := func(t *testing.T, tweak tweaker) { 434 originalHash := hash(original, true) 435 modifiable := original.Copy() 436 tweak(modifiable) 437 tweakedHash := hash(modifiable, true) 438 require.NotEqual(t, originalHash, tweakedHash) 439 } 440 441 // these tests use tweaker to modify 1 field and make the false assertion 442 // on comparing the resulting hash output 443 444 t.Run("mod address", func(t *testing.T) { 445 try(t, func(s *svc) { s.Address = "example.com" }) 446 }) 447 448 t.Run("mod name", func(t *testing.T) { 449 try(t, func(s *svc) { s.Name = "newName" }) 450 }) 451 452 t.Run("mod port label", func(t *testing.T) { 453 try(t, func(s *svc) { s.PortLabel = "newPortLabel" }) 454 }) 455 456 t.Run("mod tags", func(t *testing.T) { 457 try(t, func(s *svc) { s.Tags = []string{"new", "tags"} }) 458 }) 459 460 t.Run("mod canary tags", func(t *testing.T) { 461 try(t, func(s *svc) { s.CanaryTags = []string{"new", "tags"} }) 462 }) 463 464 t.Run("mod enable tag override", func(t *testing.T) { 465 try(t, func(s *svc) { s.EnableTagOverride = true }) 466 }) 467 468 t.Run("mod connect sidecar tags", func(t *testing.T) { 469 try(t, func(s *svc) { s.Connect.SidecarService.Tags = []string{"new", "tags"} }) 470 }) 471 472 t.Run("mod connect sidecar port", func(t *testing.T) { 473 try(t, func(s *svc) { s.Connect.SidecarService.Port = "9090" }) 474 }) 475 476 t.Run("mod connect sidecar proxy local service address", func(t *testing.T) { 477 try(t, func(s *svc) { s.Connect.SidecarService.Proxy.LocalServiceAddress = "1.1.1.1" }) 478 }) 479 480 t.Run("mod connect sidecar proxy local service port", func(t *testing.T) { 481 try(t, func(s *svc) { s.Connect.SidecarService.Proxy.LocalServicePort = 9999 }) 482 }) 483 484 t.Run("mod connect sidecar proxy config", func(t *testing.T) { 485 try(t, func(s *svc) { s.Connect.SidecarService.Proxy.Config = map[string]interface{}{"foo": "baz"} }) 486 }) 487 488 t.Run("mod connect sidecar proxy upstream destination name", func(t *testing.T) { 489 try(t, func(s *svc) { s.Connect.SidecarService.Proxy.Upstreams[0].DestinationName = "dest2" }) 490 }) 491 492 t.Run("mod connect sidecar proxy upstream destination namespace", func(t *testing.T) { 493 try(t, func(s *svc) { s.Connect.SidecarService.Proxy.Upstreams[0].DestinationNamespace = "ns3" }) 494 }) 495 496 t.Run("mod connect sidecar proxy upstream destination local bind port", func(t *testing.T) { 497 try(t, func(s *svc) { s.Connect.SidecarService.Proxy.Upstreams[0].LocalBindPort = 29999 }) 498 }) 499 500 t.Run("mod connect sidecar proxy upstream config", func(t *testing.T) { 501 try(t, func(s *svc) { s.Connect.SidecarService.Proxy.Upstreams[0].Config = map[string]any{"foo": "baz"} }) 502 }) 503 } 504 505 func TestConsulConnect_Validate(t *testing.T) { 506 ci.Parallel(t) 507 508 c := &ConsulConnect{} 509 510 // An empty Connect block is invalid 511 require.Error(t, c.Validate()) 512 513 c.Native = true 514 require.NoError(t, c.Validate()) 515 516 // Native=true + Sidecar!=nil is invalid 517 c.SidecarService = &ConsulSidecarService{} 518 require.Error(t, c.Validate()) 519 520 c.Native = false 521 require.NoError(t, c.Validate()) 522 } 523 524 func TestConsulConnect_CopyEqual(t *testing.T) { 525 ci.Parallel(t) 526 527 c := &ConsulConnect{ 528 SidecarService: &ConsulSidecarService{ 529 Tags: []string{"tag1", "tag2"}, 530 Port: "9001", 531 Proxy: &ConsulProxy{ 532 LocalServiceAddress: "127.0.0.1", 533 LocalServicePort: 8080, 534 Upstreams: []ConsulUpstream{ 535 { 536 DestinationName: "up1", 537 DestinationNamespace: "ns2", 538 LocalBindPort: 9002, 539 }, 540 { 541 DestinationName: "up2", 542 DestinationNamespace: "ns2", 543 LocalBindPort: 9003, 544 }, 545 }, 546 Config: map[string]interface{}{ 547 "foo": 1, 548 }, 549 }, 550 Meta: map[string]string{ 551 "test-key": "test-value", 552 }, 553 }, 554 } 555 556 require.NoError(t, c.Validate()) 557 558 // Copies should be equivalent 559 o := c.Copy() 560 require.True(t, c.Equal(o)) 561 562 o.SidecarService.Proxy.Upstreams = nil 563 require.False(t, c.Equal(o)) 564 } 565 566 func TestConsulConnect_GatewayProxy_CopyEqual(t *testing.T) { 567 ci.Parallel(t) 568 569 c := &ConsulGatewayProxy{ 570 ConnectTimeout: pointer.Of(1 * time.Second), 571 EnvoyGatewayBindTaggedAddresses: false, 572 EnvoyGatewayBindAddresses: make(map[string]*ConsulGatewayBindAddress), 573 } 574 575 require.NoError(t, c.Validate()) 576 577 // Copies should be equivalent 578 o := c.Copy() 579 require.Equal(t, c, o) 580 require.True(t, c.Equal(o)) 581 } 582 583 func TestSidecarTask_MergeIntoTask(t *testing.T) { 584 ci.Parallel(t) 585 586 task := MockJob().TaskGroups[0].Tasks[0] 587 sTask := &SidecarTask{ 588 Name: "sidecar", 589 Driver: "sidecar", 590 User: "test", 591 Config: map[string]interface{}{ 592 "foo": "bar", 593 }, 594 Resources: &Resources{ 595 CPU: 10000, 596 MemoryMB: 10000, 597 }, 598 Env: map[string]string{ 599 "sidecar": "proxy", 600 }, 601 Meta: map[string]string{ 602 "abc": "123", 603 }, 604 KillTimeout: pointer.Of(15 * time.Second), 605 LogConfig: &LogConfig{ 606 MaxFiles: 3, 607 }, 608 ShutdownDelay: pointer.Of(5 * time.Second), 609 KillSignal: "SIGABRT", 610 } 611 612 expected := task.Copy() 613 expected.Name = "sidecar" 614 expected.Driver = "sidecar" 615 expected.User = "test" 616 expected.Config = map[string]interface{}{ 617 "foo": "bar", 618 } 619 expected.Resources.CPU = 10000 620 expected.Resources.MemoryMB = 10000 621 expected.Env["sidecar"] = "proxy" 622 expected.Meta["abc"] = "123" 623 expected.KillTimeout = 15 * time.Second 624 expected.LogConfig.MaxFiles = 3 625 expected.ShutdownDelay = 5 * time.Second 626 expected.KillSignal = "SIGABRT" 627 628 sTask.MergeIntoTask(task) 629 require.Exactly(t, expected, task) 630 631 // Check that changing just driver config doesn't replace map 632 sTask.Config["abc"] = 123 633 expected.Config["abc"] = 123 634 635 sTask.MergeIntoTask(task) 636 require.Exactly(t, expected, task) 637 } 638 639 func TestSidecarTask_Equal(t *testing.T) { 640 ci.Parallel(t) 641 642 original := &SidecarTask{ 643 Name: "sidecar-task-1", 644 Driver: "docker", 645 User: "nobody", 646 Config: map[string]interface{}{"foo": 1}, 647 Env: map[string]string{"color": "blue"}, 648 Resources: &Resources{MemoryMB: 300}, 649 Meta: map[string]string{"index": "1"}, 650 KillTimeout: pointer.Of(2 * time.Second), 651 LogConfig: &LogConfig{ 652 MaxFiles: 2, 653 MaxFileSizeMB: 300, 654 }, 655 ShutdownDelay: pointer.Of(10 * time.Second), 656 KillSignal: "SIGTERM", 657 } 658 659 t.Run("unmodified", func(t *testing.T) { 660 duplicate := original.Copy() 661 require.True(t, duplicate.Equal(original)) 662 }) 663 664 type st = SidecarTask 665 type tweaker = func(task *st) 666 667 try := func(t *testing.T, tweak tweaker) { 668 modified := original.Copy() 669 tweak(modified) 670 require.NotEqual(t, original, modified) 671 } 672 673 t.Run("mod name", func(t *testing.T) { 674 try(t, func(s *st) { s.Name = "sidecar-task-2" }) 675 }) 676 677 t.Run("mod driver", func(t *testing.T) { 678 try(t, func(s *st) { s.Driver = "exec" }) 679 }) 680 681 t.Run("mod user", func(t *testing.T) { 682 try(t, func(s *st) { s.User = "root" }) 683 }) 684 685 t.Run("mod config", func(t *testing.T) { 686 try(t, func(s *st) { s.Config = map[string]interface{}{"foo": 2} }) 687 }) 688 689 t.Run("mod env", func(t *testing.T) { 690 try(t, func(s *st) { s.Env = map[string]string{"color": "red"} }) 691 }) 692 693 t.Run("mod resources", func(t *testing.T) { 694 try(t, func(s *st) { s.Resources = &Resources{MemoryMB: 200} }) 695 }) 696 697 t.Run("mod meta", func(t *testing.T) { 698 try(t, func(s *st) { s.Meta = map[string]string{"index": "2"} }) 699 }) 700 701 t.Run("mod kill timeout", func(t *testing.T) { 702 try(t, func(s *st) { s.KillTimeout = pointer.Of(3 * time.Second) }) 703 }) 704 705 t.Run("mod log config", func(t *testing.T) { 706 try(t, func(s *st) { s.LogConfig = &LogConfig{MaxFiles: 3} }) 707 }) 708 709 t.Run("mod shutdown delay", func(t *testing.T) { 710 try(t, func(s *st) { s.ShutdownDelay = pointer.Of(20 * time.Second) }) 711 }) 712 713 t.Run("mod kill signal", func(t *testing.T) { 714 try(t, func(s *st) { s.KillSignal = "SIGHUP" }) 715 }) 716 } 717 718 func TestConsulUpstream_upstreamEqual(t *testing.T) { 719 ci.Parallel(t) 720 721 up := func(name string, port int) ConsulUpstream { 722 return ConsulUpstream{ 723 DestinationName: name, 724 LocalBindPort: port, 725 } 726 } 727 728 t.Run("size mismatch", func(t *testing.T) { 729 a := []ConsulUpstream{up("foo", 8000)} 730 b := []ConsulUpstream{up("foo", 8000), up("bar", 9000)} 731 must.False(t, upstreamsEquals(a, b)) 732 }) 733 734 t.Run("different", func(t *testing.T) { 735 a := []ConsulUpstream{up("bar", 9000)} 736 b := []ConsulUpstream{up("foo", 8000)} 737 must.False(t, upstreamsEquals(a, b)) 738 }) 739 740 t.Run("different namespace", func(t *testing.T) { 741 a := []ConsulUpstream{up("foo", 8000)} 742 a[0].DestinationNamespace = "ns1" 743 744 b := []ConsulUpstream{up("foo", 8000)} 745 b[0].DestinationNamespace = "ns2" 746 747 must.False(t, upstreamsEquals(a, b)) 748 }) 749 750 t.Run("different mesh_gateway", func(t *testing.T) { 751 a := []ConsulUpstream{{DestinationName: "foo", MeshGateway: ConsulMeshGateway{Mode: "local"}}} 752 b := []ConsulUpstream{{DestinationName: "foo", MeshGateway: ConsulMeshGateway{Mode: "remote"}}} 753 must.False(t, upstreamsEquals(a, b)) 754 }) 755 756 t.Run("different opaque config", func(t *testing.T) { 757 a := []ConsulUpstream{{Config: map[string]any{"foo": 1}}} 758 b := []ConsulUpstream{{Config: map[string]any{"foo": 2}}} 759 must.False(t, upstreamsEquals(a, b)) 760 }) 761 762 t.Run("identical", func(t *testing.T) { 763 a := []ConsulUpstream{up("foo", 8000), up("bar", 9000)} 764 b := []ConsulUpstream{up("foo", 8000), up("bar", 9000)} 765 must.True(t, upstreamsEquals(a, b)) 766 }) 767 768 t.Run("unsorted", func(t *testing.T) { 769 a := []ConsulUpstream{up("foo", 8000), up("bar", 9000)} 770 b := []ConsulUpstream{up("bar", 9000), up("foo", 8000)} 771 must.True(t, upstreamsEquals(a, b)) 772 }) 773 } 774 775 func TestConsulExposePath_exposePathsEqual(t *testing.T) { 776 ci.Parallel(t) 777 778 expose := func(path, protocol, listen string, local int) ConsulExposePath { 779 return ConsulExposePath{ 780 Path: path, 781 Protocol: protocol, 782 LocalPathPort: local, 783 ListenerPort: listen, 784 } 785 } 786 787 t.Run("size mismatch", func(t *testing.T) { 788 a := []ConsulExposePath{expose("/1", "http", "myPort", 8000)} 789 b := []ConsulExposePath{expose("/1", "http", "myPort", 8000), expose("/2", "http", "myPort", 8000)} 790 require.False(t, exposePathsEqual(a, b)) 791 }) 792 793 t.Run("different", func(t *testing.T) { 794 a := []ConsulExposePath{expose("/1", "http", "myPort", 8000)} 795 b := []ConsulExposePath{expose("/2", "http", "myPort", 8000)} 796 require.False(t, exposePathsEqual(a, b)) 797 }) 798 799 t.Run("identical", func(t *testing.T) { 800 a := []ConsulExposePath{expose("/1", "http", "myPort", 8000)} 801 b := []ConsulExposePath{expose("/1", "http", "myPort", 8000)} 802 require.True(t, exposePathsEqual(a, b)) 803 }) 804 805 t.Run("unsorted", func(t *testing.T) { 806 a := []ConsulExposePath{expose("/2", "http", "myPort", 8000), expose("/1", "http", "myPort", 8000)} 807 b := []ConsulExposePath{expose("/1", "http", "myPort", 8000), expose("/2", "http", "myPort", 8000)} 808 require.True(t, exposePathsEqual(a, b)) 809 }) 810 } 811 812 func TestConsulExposeConfig_Copy(t *testing.T) { 813 ci.Parallel(t) 814 815 require.Nil(t, (*ConsulExposeConfig)(nil).Copy()) 816 require.Equal(t, &ConsulExposeConfig{ 817 Paths: []ConsulExposePath{{ 818 Path: "/health", 819 }}, 820 }, (&ConsulExposeConfig{ 821 Paths: []ConsulExposePath{{ 822 Path: "/health", 823 }}, 824 }).Copy()) 825 } 826 827 func TestConsulExposeConfig_Equal(t *testing.T) { 828 ci.Parallel(t) 829 830 require.True(t, (*ConsulExposeConfig)(nil).Equal(nil)) 831 require.True(t, (&ConsulExposeConfig{ 832 Paths: []ConsulExposePath{{ 833 Path: "/health", 834 }}, 835 }).Equal(&ConsulExposeConfig{ 836 Paths: []ConsulExposePath{{ 837 Path: "/health", 838 }}, 839 })) 840 } 841 842 func TestConsulSidecarService_Copy(t *testing.T) { 843 ci.Parallel(t) 844 845 t.Run("nil", func(t *testing.T) { 846 s := (*ConsulSidecarService)(nil) 847 result := s.Copy() 848 require.Nil(t, result) 849 }) 850 851 t.Run("not nil", func(t *testing.T) { 852 s := &ConsulSidecarService{ 853 Tags: []string{"foo", "bar"}, 854 Port: "port1", 855 Proxy: &ConsulProxy{LocalServiceAddress: "10.0.0.1"}, 856 Meta: map[string]string{ 857 "test-key": "test-value", 858 }, 859 } 860 result := s.Copy() 861 require.Equal(t, &ConsulSidecarService{ 862 Tags: []string{"foo", "bar"}, 863 Port: "port1", 864 Proxy: &ConsulProxy{LocalServiceAddress: "10.0.0.1"}, 865 Meta: map[string]string{ 866 "test-key": "test-value", 867 }, 868 }, result) 869 }) 870 } 871 872 var ( 873 consulIngressGateway1 = &ConsulGateway{ 874 Proxy: &ConsulGatewayProxy{ 875 ConnectTimeout: pointer.Of(1 * time.Second), 876 EnvoyGatewayBindTaggedAddresses: true, 877 EnvoyGatewayBindAddresses: map[string]*ConsulGatewayBindAddress{ 878 "listener1": {Address: "10.0.0.1", Port: 2001}, 879 "listener2": {Address: "10.0.0.1", Port: 2002}, 880 }, 881 EnvoyGatewayNoDefaultBind: true, 882 Config: map[string]interface{}{ 883 "foo": 1, 884 }, 885 }, 886 Ingress: &ConsulIngressConfigEntry{ 887 TLS: &ConsulGatewayTLSConfig{ 888 Enabled: true, 889 }, 890 Listeners: []*ConsulIngressListener{{ 891 Port: 3000, 892 Protocol: "http", 893 Services: []*ConsulIngressService{{ 894 Name: "service1", 895 Hosts: []string{"10.0.0.1", "10.0.0.1:3000"}, 896 }, { 897 Name: "service2", 898 Hosts: []string{"10.0.0.2", "10.0.0.2:3000"}, 899 }}, 900 }, { 901 Port: 3001, 902 Protocol: "tcp", 903 Services: []*ConsulIngressService{{ 904 Name: "service3", 905 }}, 906 }}, 907 }, 908 } 909 910 consulTerminatingGateway1 = &ConsulGateway{ 911 Proxy: &ConsulGatewayProxy{ 912 ConnectTimeout: pointer.Of(1 * time.Second), 913 EnvoyDNSDiscoveryType: "STRICT_DNS", 914 EnvoyGatewayBindAddresses: nil, 915 }, 916 Terminating: &ConsulTerminatingConfigEntry{ 917 Services: []*ConsulLinkedService{{ 918 Name: "linked-service1", 919 CAFile: "ca.pem", 920 CertFile: "cert.pem", 921 KeyFile: "key.pem", 922 SNI: "service1.consul", 923 }, { 924 Name: "linked-service2", 925 }}, 926 }, 927 } 928 929 consulMeshGateway1 = &ConsulGateway{ 930 Proxy: &ConsulGatewayProxy{ 931 ConnectTimeout: pointer.Of(1 * time.Second), 932 }, 933 Mesh: &ConsulMeshConfigEntry{ 934 // nothing 935 }, 936 } 937 ) 938 939 func TestConsulGateway_Prefix(t *testing.T) { 940 ci.Parallel(t) 941 942 t.Run("ingress", func(t *testing.T) { 943 result := (&ConsulGateway{Ingress: new(ConsulIngressConfigEntry)}).Prefix() 944 require.Equal(t, ConnectIngressPrefix, result) 945 }) 946 947 t.Run("terminating", func(t *testing.T) { 948 result := (&ConsulGateway{Terminating: new(ConsulTerminatingConfigEntry)}).Prefix() 949 require.Equal(t, ConnectTerminatingPrefix, result) 950 }) 951 952 t.Run("mesh", func(t *testing.T) { 953 result := (&ConsulGateway{Mesh: new(ConsulMeshConfigEntry)}).Prefix() 954 require.Equal(t, ConnectMeshPrefix, result) 955 }) 956 } 957 958 func TestConsulGateway_Copy(t *testing.T) { 959 ci.Parallel(t) 960 961 t.Run("nil", func(t *testing.T) { 962 g := (*ConsulGateway)(nil) 963 result := g.Copy() 964 require.Nil(t, result) 965 }) 966 967 t.Run("as ingress", func(t *testing.T) { 968 result := consulIngressGateway1.Copy() 969 require.Equal(t, consulIngressGateway1, result) 970 require.True(t, result.Equal(consulIngressGateway1)) 971 require.True(t, consulIngressGateway1.Equal(result)) 972 }) 973 974 t.Run("as terminating", func(t *testing.T) { 975 result := consulTerminatingGateway1.Copy() 976 require.Equal(t, consulTerminatingGateway1, result) 977 require.True(t, result.Equal(consulTerminatingGateway1)) 978 require.True(t, consulTerminatingGateway1.Equal(result)) 979 }) 980 981 t.Run("as mesh", func(t *testing.T) { 982 result := consulMeshGateway1.Copy() 983 require.Equal(t, consulMeshGateway1, result) 984 require.True(t, result.Equal(consulMeshGateway1)) 985 require.True(t, consulMeshGateway1.Equal(result)) 986 }) 987 } 988 989 func TestConsulGateway_Equal_mesh(t *testing.T) { 990 ci.Parallel(t) 991 992 t.Run("nil", func(t *testing.T) { 993 a := (*ConsulGateway)(nil) 994 b := (*ConsulGateway)(nil) 995 require.True(t, a.Equal(b)) 996 require.False(t, a.Equal(consulMeshGateway1)) 997 require.False(t, consulMeshGateway1.Equal(a)) 998 }) 999 1000 t.Run("reflexive", func(t *testing.T) { 1001 require.True(t, consulMeshGateway1.Equal(consulMeshGateway1)) 1002 }) 1003 } 1004 1005 func TestConsulGateway_Equal_ingress(t *testing.T) { 1006 ci.Parallel(t) 1007 1008 t.Run("nil", func(t *testing.T) { 1009 a := (*ConsulGateway)(nil) 1010 b := (*ConsulGateway)(nil) 1011 require.True(t, a.Equal(b)) 1012 require.False(t, a.Equal(consulIngressGateway1)) 1013 require.False(t, consulIngressGateway1.Equal(a)) 1014 }) 1015 1016 original := consulIngressGateway1.Copy() 1017 1018 type cg = ConsulGateway 1019 type tweaker = func(g *cg) 1020 1021 t.Run("reflexive", func(t *testing.T) { 1022 require.True(t, original.Equal(original)) 1023 }) 1024 1025 try := func(t *testing.T, tweak tweaker) { 1026 modifiable := original.Copy() 1027 tweak(modifiable) 1028 require.False(t, original.Equal(modifiable)) 1029 require.False(t, modifiable.Equal(original)) 1030 require.True(t, modifiable.Equal(modifiable)) 1031 } 1032 1033 // proxy block equality checks 1034 1035 t.Run("mod gateway timeout", func(t *testing.T) { 1036 try(t, func(g *cg) { g.Proxy.ConnectTimeout = pointer.Of(9 * time.Second) }) 1037 }) 1038 1039 t.Run("mod gateway envoy_gateway_bind_tagged_addresses", func(t *testing.T) { 1040 try(t, func(g *cg) { g.Proxy.EnvoyGatewayBindTaggedAddresses = false }) 1041 }) 1042 1043 t.Run("mod gateway envoy_gateway_bind_addresses", func(t *testing.T) { 1044 try(t, func(g *cg) { 1045 g.Proxy.EnvoyGatewayBindAddresses = map[string]*ConsulGatewayBindAddress{ 1046 "listener3": {Address: "9.9.9.9", Port: 9999}, 1047 } 1048 }) 1049 }) 1050 1051 t.Run("mod gateway envoy_gateway_no_default_bind", func(t *testing.T) { 1052 try(t, func(g *cg) { g.Proxy.EnvoyGatewayNoDefaultBind = false }) 1053 }) 1054 1055 t.Run("mod gateway config", func(t *testing.T) { 1056 try(t, func(g *cg) { 1057 g.Proxy.Config = map[string]interface{}{ 1058 "foo": 2, 1059 } 1060 }) 1061 }) 1062 1063 // ingress config entry equality checks 1064 1065 t.Run("mod ingress tls", func(t *testing.T) { 1066 try(t, func(g *cg) { g.Ingress.TLS = nil }) 1067 try(t, func(g *cg) { g.Ingress.TLS.Enabled = false }) 1068 }) 1069 1070 t.Run("mod ingress listeners count", func(t *testing.T) { 1071 try(t, func(g *cg) { g.Ingress.Listeners = g.Ingress.Listeners[:1] }) 1072 }) 1073 1074 t.Run("mod ingress listeners port", func(t *testing.T) { 1075 try(t, func(g *cg) { g.Ingress.Listeners[0].Port = 7777 }) 1076 }) 1077 1078 t.Run("mod ingress listeners protocol", func(t *testing.T) { 1079 try(t, func(g *cg) { g.Ingress.Listeners[0].Protocol = "tcp" }) 1080 }) 1081 1082 t.Run("mod ingress listeners services count", func(t *testing.T) { 1083 try(t, func(g *cg) { g.Ingress.Listeners[0].Services = g.Ingress.Listeners[0].Services[:1] }) 1084 }) 1085 1086 t.Run("mod ingress listeners services name", func(t *testing.T) { 1087 try(t, func(g *cg) { g.Ingress.Listeners[0].Services[0].Name = "serviceX" }) 1088 }) 1089 1090 t.Run("mod ingress listeners services hosts count", func(t *testing.T) { 1091 try(t, func(g *cg) { g.Ingress.Listeners[0].Services[0].Hosts = g.Ingress.Listeners[0].Services[0].Hosts[:1] }) 1092 }) 1093 1094 t.Run("mod ingress listeners services hosts content", func(t *testing.T) { 1095 try(t, func(g *cg) { g.Ingress.Listeners[0].Services[0].Hosts[0] = "255.255.255.255" }) 1096 }) 1097 } 1098 1099 func TestConsulGateway_Equal_terminating(t *testing.T) { 1100 ci.Parallel(t) 1101 1102 original := consulTerminatingGateway1.Copy() 1103 1104 type cg = ConsulGateway 1105 type tweaker = func(c *cg) 1106 1107 t.Run("reflexive", func(t *testing.T) { 1108 require.True(t, original.Equal(original)) 1109 }) 1110 1111 try := func(t *testing.T, tweak tweaker) { 1112 modifiable := original.Copy() 1113 tweak(modifiable) 1114 require.False(t, original.Equal(modifiable)) 1115 require.False(t, modifiable.Equal(original)) 1116 require.True(t, modifiable.Equal(modifiable)) 1117 } 1118 1119 // proxy block equality checks 1120 1121 t.Run("mod dns discovery type", func(t *testing.T) { 1122 try(t, func(g *cg) { g.Proxy.EnvoyDNSDiscoveryType = "LOGICAL_DNS" }) 1123 }) 1124 1125 // terminating config entry equality checks 1126 1127 t.Run("mod terminating services count", func(t *testing.T) { 1128 try(t, func(g *cg) { g.Terminating.Services = g.Terminating.Services[:1] }) 1129 }) 1130 1131 t.Run("mod terminating services name", func(t *testing.T) { 1132 try(t, func(g *cg) { g.Terminating.Services[0].Name = "foo" }) 1133 }) 1134 1135 t.Run("mod terminating services ca_file", func(t *testing.T) { 1136 try(t, func(g *cg) { g.Terminating.Services[0].CAFile = "foo.pem" }) 1137 }) 1138 1139 t.Run("mod terminating services cert_file", func(t *testing.T) { 1140 try(t, func(g *cg) { g.Terminating.Services[0].CertFile = "foo.pem" }) 1141 }) 1142 1143 t.Run("mod terminating services key_file", func(t *testing.T) { 1144 try(t, func(g *cg) { g.Terminating.Services[0].KeyFile = "foo.pem" }) 1145 }) 1146 1147 t.Run("mod terminating services sni", func(t *testing.T) { 1148 try(t, func(g *cg) { g.Terminating.Services[0].SNI = "foo.consul" }) 1149 }) 1150 } 1151 1152 func TestConsulGateway_ingressServicesEqual(t *testing.T) { 1153 ci.Parallel(t) 1154 1155 igs1 := []*ConsulIngressService{{ 1156 Name: "service1", 1157 Hosts: []string{"host1", "host2"}, 1158 }, { 1159 Name: "service2", 1160 Hosts: []string{"host3"}, 1161 }} 1162 1163 require.False(t, ingressServicesEqual(igs1, nil)) 1164 require.True(t, ingressServicesEqual(igs1, igs1)) 1165 1166 reversed := []*ConsulIngressService{ 1167 igs1[1], igs1[0], // services reversed 1168 } 1169 1170 require.True(t, ingressServicesEqual(igs1, reversed)) 1171 1172 hostOrder := []*ConsulIngressService{{ 1173 Name: "service1", 1174 Hosts: []string{"host2", "host1"}, // hosts reversed 1175 }, { 1176 Name: "service2", 1177 Hosts: []string{"host3"}, 1178 }} 1179 1180 require.True(t, ingressServicesEqual(igs1, hostOrder)) 1181 } 1182 1183 func TestConsulGateway_ingressListenersEqual(t *testing.T) { 1184 ci.Parallel(t) 1185 1186 ils1 := []*ConsulIngressListener{{ 1187 Port: 2000, 1188 Protocol: "http", 1189 Services: []*ConsulIngressService{{ 1190 Name: "service1", 1191 Hosts: []string{"host1", "host2"}, 1192 }}, 1193 }, { 1194 Port: 2001, 1195 Protocol: "tcp", 1196 Services: []*ConsulIngressService{{ 1197 Name: "service2", 1198 }}, 1199 }} 1200 1201 require.False(t, ingressListenersEqual(ils1, nil)) 1202 1203 reversed := []*ConsulIngressListener{ 1204 ils1[1], ils1[0], 1205 } 1206 1207 require.True(t, ingressListenersEqual(ils1, reversed)) 1208 } 1209 1210 func TestConsulGateway_Validate(t *testing.T) { 1211 ci.Parallel(t) 1212 1213 t.Run("bad proxy", func(t *testing.T) { 1214 err := (&ConsulGateway{ 1215 Proxy: &ConsulGatewayProxy{ 1216 ConnectTimeout: nil, 1217 }, 1218 Ingress: nil, 1219 }).Validate() 1220 require.EqualError(t, err, "Consul Gateway Proxy connection_timeout must be set") 1221 }) 1222 1223 t.Run("bad ingress config entry", func(t *testing.T) { 1224 err := (&ConsulGateway{ 1225 Ingress: &ConsulIngressConfigEntry{ 1226 Listeners: nil, 1227 }, 1228 }).Validate() 1229 require.EqualError(t, err, "Consul Ingress Gateway requires at least one listener") 1230 }) 1231 1232 t.Run("bad terminating config entry", func(t *testing.T) { 1233 err := (&ConsulGateway{ 1234 Terminating: &ConsulTerminatingConfigEntry{ 1235 Services: nil, 1236 }, 1237 }).Validate() 1238 require.EqualError(t, err, "Consul Terminating Gateway requires at least one service") 1239 }) 1240 1241 t.Run("no config entry set", func(t *testing.T) { 1242 err := (&ConsulGateway{ 1243 Ingress: nil, 1244 Terminating: nil, 1245 Mesh: nil, 1246 }).Validate() 1247 require.EqualError(t, err, "One Consul Gateway Configuration must be set") 1248 }) 1249 1250 t.Run("multiple config entries set", func(t *testing.T) { 1251 err := (&ConsulGateway{ 1252 Ingress: &ConsulIngressConfigEntry{ 1253 Listeners: []*ConsulIngressListener{{ 1254 Port: 1111, 1255 Protocol: "tcp", 1256 Services: []*ConsulIngressService{{ 1257 Name: "service1", 1258 }}, 1259 }}, 1260 }, 1261 Terminating: &ConsulTerminatingConfigEntry{ 1262 Services: []*ConsulLinkedService{{ 1263 Name: "linked-service1", 1264 }}, 1265 }, 1266 }).Validate() 1267 require.EqualError(t, err, "One Consul Gateway Configuration must be set") 1268 }) 1269 1270 t.Run("ok mesh", func(t *testing.T) { 1271 err := (&ConsulGateway{ 1272 Mesh: new(ConsulMeshConfigEntry), 1273 }).Validate() 1274 require.NoError(t, err) 1275 }) 1276 } 1277 1278 func TestConsulGatewayBindAddress_Validate(t *testing.T) { 1279 ci.Parallel(t) 1280 1281 t.Run("no address", func(t *testing.T) { 1282 err := (&ConsulGatewayBindAddress{ 1283 Address: "", 1284 Port: 2000, 1285 }).Validate() 1286 require.EqualError(t, err, "Consul Gateway Bind Address must be set") 1287 }) 1288 1289 t.Run("invalid port", func(t *testing.T) { 1290 err := (&ConsulGatewayBindAddress{ 1291 Address: "10.0.0.1", 1292 Port: 0, 1293 }).Validate() 1294 require.EqualError(t, err, "Consul Gateway Bind Address must set valid Port") 1295 }) 1296 1297 t.Run("ok", func(t *testing.T) { 1298 err := (&ConsulGatewayBindAddress{ 1299 Address: "10.0.0.1", 1300 Port: 2000, 1301 }).Validate() 1302 require.NoError(t, err) 1303 }) 1304 } 1305 1306 func TestConsulGatewayProxy_Validate(t *testing.T) { 1307 ci.Parallel(t) 1308 1309 t.Run("no timeout", func(t *testing.T) { 1310 err := (&ConsulGatewayProxy{ 1311 ConnectTimeout: nil, 1312 }).Validate() 1313 require.EqualError(t, err, "Consul Gateway Proxy connection_timeout must be set") 1314 }) 1315 1316 t.Run("invalid bind address", func(t *testing.T) { 1317 err := (&ConsulGatewayProxy{ 1318 ConnectTimeout: pointer.Of(1 * time.Second), 1319 EnvoyGatewayBindAddresses: map[string]*ConsulGatewayBindAddress{ 1320 "service1": { 1321 Address: "10.0.0.1", 1322 Port: 0, 1323 }}, 1324 }).Validate() 1325 require.EqualError(t, err, "Consul Gateway Bind Address must set valid Port") 1326 }) 1327 1328 t.Run("invalid dns discovery type", func(t *testing.T) { 1329 err := (&ConsulGatewayProxy{ 1330 ConnectTimeout: pointer.Of(1 * time.Second), 1331 EnvoyDNSDiscoveryType: "RANDOM_DNS", 1332 }).Validate() 1333 require.EqualError(t, err, "Consul Gateway Proxy Envoy DNS Discovery type must be STRICT_DNS or LOGICAL_DNS") 1334 }) 1335 1336 t.Run("ok with nothing set", func(t *testing.T) { 1337 err := (&ConsulGatewayProxy{ 1338 ConnectTimeout: pointer.Of(1 * time.Second), 1339 }).Validate() 1340 require.NoError(t, err) 1341 }) 1342 1343 t.Run("ok with everything set", func(t *testing.T) { 1344 err := (&ConsulGatewayProxy{ 1345 ConnectTimeout: pointer.Of(1 * time.Second), 1346 EnvoyGatewayBindAddresses: map[string]*ConsulGatewayBindAddress{ 1347 "service1": { 1348 Address: "10.0.0.1", 1349 Port: 2000, 1350 }}, 1351 EnvoyGatewayBindTaggedAddresses: true, 1352 EnvoyGatewayNoDefaultBind: true, 1353 }).Validate() 1354 require.NoError(t, err) 1355 }) 1356 } 1357 1358 func TestConsulIngressService_Validate(t *testing.T) { 1359 ci.Parallel(t) 1360 1361 t.Run("invalid name", func(t *testing.T) { 1362 err := (&ConsulIngressService{ 1363 Name: "", 1364 }).Validate("http") 1365 must.EqError(t, err, "Consul Ingress Service requires a name") 1366 }) 1367 1368 t.Run("tcp extraneous hosts", func(t *testing.T) { 1369 err := (&ConsulIngressService{ 1370 Name: "service1", 1371 Hosts: []string{"host1"}, 1372 }).Validate("tcp") 1373 must.EqError(t, err, `Consul Ingress Service doesn't support associating hosts to a service for the "tcp" protocol`) 1374 }) 1375 1376 t.Run("tcp ok", func(t *testing.T) { 1377 err := (&ConsulIngressService{ 1378 Name: "service1", 1379 }).Validate("tcp") 1380 must.NoError(t, err) 1381 }) 1382 1383 t.Run("tcp with wildcard service", func(t *testing.T) { 1384 err := (&ConsulIngressService{ 1385 Name: "*", 1386 }).Validate("tcp") 1387 must.EqError(t, err, `Consul Ingress Service doesn't support wildcard name for "tcp" protocol`) 1388 }) 1389 1390 // non-"tcp" protocols should be all treated the same. 1391 for _, proto := range []string{"http", "http2", "grpc"} { 1392 t.Run(proto+" ok", func(t *testing.T) { 1393 err := (&ConsulIngressService{ 1394 Name: "service1", 1395 Hosts: []string{"host1"}, 1396 }).Validate(proto) 1397 must.NoError(t, err) 1398 }) 1399 1400 t.Run(proto+" without hosts", func(t *testing.T) { 1401 err := (&ConsulIngressService{ 1402 Name: "service1", 1403 }).Validate(proto) 1404 must.NoError(t, err, must.Sprintf(`"%s" protocol should not require hosts`, proto)) 1405 }) 1406 1407 t.Run(proto+" wildcard service", func(t *testing.T) { 1408 err := (&ConsulIngressService{ 1409 Name: "*", 1410 }).Validate(proto) 1411 must.NoError(t, err, must.Sprintf(`"%s" protocol should allow wildcard service`, proto)) 1412 }) 1413 1414 t.Run(proto+" wildcard service and host", func(t *testing.T) { 1415 err := (&ConsulIngressService{ 1416 Name: "*", 1417 Hosts: []string{"any"}, 1418 }).Validate(proto) 1419 must.EqError(t, err, `Consul Ingress Service with a wildcard "*" service name can not also specify hosts`) 1420 }) 1421 } 1422 } 1423 1424 func TestConsulIngressListener_Validate(t *testing.T) { 1425 ci.Parallel(t) 1426 1427 t.Run("invalid port", func(t *testing.T) { 1428 err := (&ConsulIngressListener{ 1429 Port: 0, 1430 Protocol: "tcp", 1431 Services: []*ConsulIngressService{{ 1432 Name: "service1", 1433 }}, 1434 }).Validate() 1435 require.EqualError(t, err, "Consul Ingress Listener requires valid Port") 1436 }) 1437 1438 t.Run("invalid protocol", func(t *testing.T) { 1439 err := (&ConsulIngressListener{ 1440 Port: 2000, 1441 Protocol: "gopher", 1442 Services: []*ConsulIngressService{{ 1443 Name: "service1", 1444 }}, 1445 }).Validate() 1446 require.EqualError(t, err, `Consul Ingress Listener requires protocol of tcp, http, http2, grpc, got "gopher"`) 1447 }) 1448 1449 t.Run("no services", func(t *testing.T) { 1450 err := (&ConsulIngressListener{ 1451 Port: 2000, 1452 Protocol: "tcp", 1453 Services: nil, 1454 }).Validate() 1455 require.EqualError(t, err, "Consul Ingress Listener requires one or more services") 1456 }) 1457 1458 t.Run("invalid service", func(t *testing.T) { 1459 err := (&ConsulIngressListener{ 1460 Port: 2000, 1461 Protocol: "tcp", 1462 Services: []*ConsulIngressService{{ 1463 Name: "", 1464 }}, 1465 }).Validate() 1466 require.EqualError(t, err, "Consul Ingress Service requires a name") 1467 }) 1468 1469 t.Run("ok", func(t *testing.T) { 1470 err := (&ConsulIngressListener{ 1471 Port: 2000, 1472 Protocol: "tcp", 1473 Services: []*ConsulIngressService{{ 1474 Name: "service1", 1475 }}, 1476 }).Validate() 1477 require.NoError(t, err) 1478 }) 1479 } 1480 1481 func TestConsulIngressConfigEntry_Validate(t *testing.T) { 1482 ci.Parallel(t) 1483 1484 t.Run("no listeners", func(t *testing.T) { 1485 err := (&ConsulIngressConfigEntry{}).Validate() 1486 require.EqualError(t, err, "Consul Ingress Gateway requires at least one listener") 1487 }) 1488 1489 t.Run("invalid listener", func(t *testing.T) { 1490 err := (&ConsulIngressConfigEntry{ 1491 Listeners: []*ConsulIngressListener{{ 1492 Port: 9000, 1493 Protocol: "tcp", 1494 }}, 1495 }).Validate() 1496 require.EqualError(t, err, "Consul Ingress Listener requires one or more services") 1497 }) 1498 1499 t.Run("full", func(t *testing.T) { 1500 err := (&ConsulIngressConfigEntry{ 1501 TLS: &ConsulGatewayTLSConfig{ 1502 Enabled: true, 1503 }, 1504 Listeners: []*ConsulIngressListener{{ 1505 Port: 9000, 1506 Protocol: "tcp", 1507 Services: []*ConsulIngressService{{ 1508 Name: "service1", 1509 }}, 1510 }}, 1511 }).Validate() 1512 require.NoError(t, err) 1513 }) 1514 } 1515 1516 func TestConsulLinkedService_Validate(t *testing.T) { 1517 ci.Parallel(t) 1518 1519 t.Run("nil", func(t *testing.T) { 1520 err := (*ConsulLinkedService)(nil).Validate() 1521 require.Nil(t, err) 1522 }) 1523 1524 t.Run("missing name", func(t *testing.T) { 1525 err := (&ConsulLinkedService{}).Validate() 1526 require.EqualError(t, err, "Consul Linked Service requires Name") 1527 }) 1528 1529 t.Run("missing ca_file", func(t *testing.T) { 1530 err := (&ConsulLinkedService{ 1531 Name: "linked-service1", 1532 CertFile: "cert_file.pem", 1533 KeyFile: "key_file.pem", 1534 }).Validate() 1535 require.EqualError(t, err, "Consul Linked Service TLS requires CAFile") 1536 }) 1537 1538 t.Run("mutual cert key", func(t *testing.T) { 1539 err := (&ConsulLinkedService{ 1540 Name: "linked-service1", 1541 CAFile: "ca_file.pem", 1542 CertFile: "cert_file.pem", 1543 }).Validate() 1544 require.EqualError(t, err, "Consul Linked Service TLS Cert and Key must both be set") 1545 }) 1546 1547 t.Run("sni without ca_file", func(t *testing.T) { 1548 err := (&ConsulLinkedService{ 1549 Name: "linked-service1", 1550 SNI: "service.consul", 1551 }).Validate() 1552 require.EqualError(t, err, "Consul Linked Service TLS SNI requires CAFile") 1553 }) 1554 1555 t.Run("minimal", func(t *testing.T) { 1556 err := (&ConsulLinkedService{ 1557 Name: "linked-service1", 1558 }).Validate() 1559 require.NoError(t, err) 1560 }) 1561 1562 t.Run("tls minimal", func(t *testing.T) { 1563 err := (&ConsulLinkedService{ 1564 Name: "linked-service1", 1565 CAFile: "ca_file.pem", 1566 }).Validate() 1567 require.NoError(t, err) 1568 }) 1569 1570 t.Run("tls mutual", func(t *testing.T) { 1571 err := (&ConsulLinkedService{ 1572 Name: "linked-service1", 1573 CAFile: "ca_file.pem", 1574 CertFile: "cert_file.pem", 1575 KeyFile: "key_file.pem", 1576 }).Validate() 1577 require.NoError(t, err) 1578 }) 1579 1580 t.Run("tls sni", func(t *testing.T) { 1581 err := (&ConsulLinkedService{ 1582 Name: "linked-service1", 1583 CAFile: "ca_file.pem", 1584 SNI: "linked-service.consul", 1585 }).Validate() 1586 require.NoError(t, err) 1587 }) 1588 1589 t.Run("tls complete", func(t *testing.T) { 1590 err := (&ConsulLinkedService{ 1591 Name: "linked-service1", 1592 CAFile: "ca_file.pem", 1593 CertFile: "cert_file.pem", 1594 KeyFile: "key_file.pem", 1595 SNI: "linked-service.consul", 1596 }).Validate() 1597 require.NoError(t, err) 1598 }) 1599 } 1600 1601 func TestConsulLinkedService_Copy(t *testing.T) { 1602 ci.Parallel(t) 1603 1604 require.Nil(t, (*ConsulLinkedService)(nil).Copy()) 1605 require.Equal(t, &ConsulLinkedService{ 1606 Name: "service1", 1607 CAFile: "ca.pem", 1608 CertFile: "cert.pem", 1609 KeyFile: "key.pem", 1610 SNI: "service1.consul", 1611 }, (&ConsulLinkedService{ 1612 Name: "service1", 1613 CAFile: "ca.pem", 1614 CertFile: "cert.pem", 1615 KeyFile: "key.pem", 1616 SNI: "service1.consul", 1617 }).Copy()) 1618 } 1619 1620 func TestConsulLinkedService_linkedServicesEqual(t *testing.T) { 1621 ci.Parallel(t) 1622 1623 services := []*ConsulLinkedService{{ 1624 Name: "service1", 1625 CAFile: "ca.pem", 1626 }, { 1627 Name: "service2", 1628 CAFile: "ca.pem", 1629 }} 1630 1631 require.False(t, linkedServicesEqual(services, nil)) 1632 require.True(t, linkedServicesEqual(services, services)) 1633 1634 reversed := []*ConsulLinkedService{ 1635 services[1], services[0], // reversed 1636 } 1637 1638 require.True(t, linkedServicesEqual(services, reversed)) 1639 1640 different := []*ConsulLinkedService{ 1641 services[0], { 1642 Name: "service2", 1643 CAFile: "ca.pem", 1644 SNI: "service2.consul", 1645 }, 1646 } 1647 1648 require.False(t, linkedServicesEqual(services, different)) 1649 } 1650 1651 func TestConsulTerminatingConfigEntry_Validate(t *testing.T) { 1652 ci.Parallel(t) 1653 1654 t.Run("nil", func(t *testing.T) { 1655 err := (*ConsulTerminatingConfigEntry)(nil).Validate() 1656 require.NoError(t, err) 1657 }) 1658 1659 t.Run("no services", func(t *testing.T) { 1660 err := (&ConsulTerminatingConfigEntry{ 1661 Services: make([]*ConsulLinkedService, 0), 1662 }).Validate() 1663 require.EqualError(t, err, "Consul Terminating Gateway requires at least one service") 1664 }) 1665 1666 t.Run("service invalid", func(t *testing.T) { 1667 err := (&ConsulTerminatingConfigEntry{ 1668 Services: []*ConsulLinkedService{{ 1669 Name: "", 1670 }}, 1671 }).Validate() 1672 require.EqualError(t, err, "Consul Linked Service requires Name") 1673 }) 1674 1675 t.Run("ok", func(t *testing.T) { 1676 err := (&ConsulTerminatingConfigEntry{ 1677 Services: []*ConsulLinkedService{{ 1678 Name: "service1", 1679 }}, 1680 }).Validate() 1681 require.NoError(t, err) 1682 }) 1683 } 1684 1685 func TestConsulMeshGateway_Copy(t *testing.T) { 1686 ci.Parallel(t) 1687 1688 require.Nil(t, (*ConsulMeshGateway)(nil)) 1689 require.Equal(t, &ConsulMeshGateway{ 1690 Mode: "remote", 1691 }, &ConsulMeshGateway{ 1692 Mode: "remote", 1693 }) 1694 } 1695 1696 func TestConsulMeshGateway_Equal(t *testing.T) { 1697 ci.Parallel(t) 1698 1699 c := ConsulMeshGateway{Mode: "local"} 1700 require.False(t, c.Equal(ConsulMeshGateway{})) 1701 require.True(t, c.Equal(c)) 1702 1703 o := ConsulMeshGateway{Mode: "remote"} 1704 require.False(t, c.Equal(o)) 1705 } 1706 1707 func TestConsulMeshGateway_Validate(t *testing.T) { 1708 ci.Parallel(t) 1709 1710 t.Run("nil", func(t *testing.T) { 1711 err := (*ConsulMeshGateway)(nil).Validate() 1712 require.NoError(t, err) 1713 }) 1714 1715 t.Run("mode invalid", func(t *testing.T) { 1716 err := (&ConsulMeshGateway{Mode: "banana"}).Validate() 1717 require.EqualError(t, err, `Connect mesh_gateway mode "banana" not supported`) 1718 }) 1719 1720 t.Run("ok", func(t *testing.T) { 1721 err := (&ConsulMeshGateway{Mode: "local"}).Validate() 1722 require.NoError(t, err) 1723 }) 1724 } 1725 1726 func TestService_Validate(t *testing.T) { 1727 ci.Parallel(t) 1728 1729 testCases := []struct { 1730 input *Service 1731 expErr bool 1732 expErrStr string 1733 name string 1734 }{ 1735 { 1736 name: "base service", 1737 input: &Service{ 1738 Name: "testservice", 1739 }, 1740 expErr: false, 1741 }, 1742 { 1743 name: "Native Connect without task name", 1744 input: &Service{ 1745 Name: "testservice", 1746 Connect: &ConsulConnect{ 1747 Native: true, 1748 }, 1749 }, 1750 expErr: false, // gets set automatically 1751 }, 1752 { 1753 name: "Native Connect with task name", 1754 input: &Service{ 1755 Name: "testservice", 1756 TaskName: "testtask", 1757 Connect: &ConsulConnect{ 1758 Native: true, 1759 }, 1760 }, 1761 expErr: false, 1762 }, 1763 { 1764 name: "Native Connect with Sidecar", 1765 input: &Service{ 1766 Name: "testservice", 1767 TaskName: "testtask", 1768 Connect: &ConsulConnect{ 1769 Native: true, 1770 SidecarService: &ConsulSidecarService{}, 1771 }, 1772 }, 1773 expErr: true, 1774 expErrStr: "Consul Connect must be exclusively native", 1775 }, 1776 { 1777 name: "provider nomad with checks", 1778 input: &Service{ 1779 Name: "testservice", 1780 Provider: "nomad", 1781 PortLabel: "port", 1782 Checks: []*ServiceCheck{ 1783 { 1784 Name: "servicecheck", 1785 Type: "http", 1786 Path: "/", 1787 Interval: 1 * time.Second, 1788 Timeout: 3 * time.Second, 1789 }, 1790 { 1791 Name: "servicecheck", 1792 Type: "tcp", 1793 Interval: 1 * time.Second, 1794 Timeout: 3 * time.Second, 1795 }, 1796 }, 1797 }, 1798 expErr: false, 1799 }, 1800 { 1801 name: "provider nomad with invalid check type", 1802 input: &Service{ 1803 Name: "testservice", 1804 Provider: "nomad", 1805 Checks: []*ServiceCheck{ 1806 { 1807 Name: "servicecheck", 1808 Type: "script", 1809 }, 1810 }, 1811 }, 1812 expErr: true, 1813 }, 1814 { 1815 name: "provider nomad with connect", 1816 input: &Service{ 1817 Name: "testservice", 1818 Provider: "nomad", 1819 Connect: &ConsulConnect{ 1820 Native: true, 1821 }, 1822 }, 1823 expErr: true, 1824 expErrStr: "Service with provider nomad cannot include Connect blocks", 1825 }, 1826 { 1827 name: "provider nomad valid", 1828 input: &Service{ 1829 Name: "testservice", 1830 Provider: "nomad", 1831 }, 1832 expErr: false, 1833 }, 1834 } 1835 1836 for _, tc := range testCases { 1837 t.Run(tc.name, func(t *testing.T) { 1838 tc.input.Canonicalize("testjob", "testgroup", "testtask", "testnamespace") 1839 err := tc.input.Validate() 1840 if tc.expErr { 1841 require.Error(t, err) 1842 require.Contains(t, err.Error(), tc.expErrStr) 1843 } else { 1844 require.NoError(t, err) 1845 } 1846 }) 1847 } 1848 } 1849 1850 func TestService_Validate_Address(t *testing.T) { 1851 ci.Parallel(t) 1852 1853 try := func(mode, advertise string, exp error) { 1854 s := &Service{Name: "s1", Provider: "consul", AddressMode: mode, Address: advertise} 1855 result := s.Validate() 1856 if exp == nil { 1857 require.NoError(t, result) 1858 } else { 1859 // would be nice if multierror worked with errors.Is 1860 require.Contains(t, result.Error(), exp.Error()) 1861 } 1862 } 1863 1864 // advertise not set 1865 try("", "", nil) 1866 try("auto", "", nil) 1867 try("host", "", nil) 1868 try("alloc", "", nil) 1869 try("driver", "", nil) 1870 1871 // advertise is set 1872 try("", "example.com", nil) 1873 try("auto", "example.com", nil) 1874 try("host", "example.com", errors.New(`Service address_mode must be "auto" if address is set`)) 1875 try("alloc", "example.com", errors.New(`Service address_mode must be "auto" if address is set`)) 1876 try("driver", "example.com", errors.New(`Service address_mode must be "auto" if address is set`)) 1877 } 1878 1879 func TestService_Equal(t *testing.T) { 1880 ci.Parallel(t) 1881 1882 s := Service{ 1883 Name: "testservice", 1884 TaggedAddresses: make(map[string]string), 1885 } 1886 1887 s.Canonicalize("testjob", "testgroup", "testtask", "default") 1888 1889 o := s.Copy() 1890 1891 // Base service should be equal to copy of itself 1892 require.True(t, s.Equal(o)) 1893 1894 // create a helper to assert a diff and reset the struct 1895 assertDiff := func() { 1896 require.False(t, s.Equal(o)) 1897 o = s.Copy() 1898 require.True(t, s.Equal(o), "bug in copy") 1899 } 1900 1901 // Changing any field should cause inequality 1902 o.Name = "diff" 1903 assertDiff() 1904 1905 o.Address = "diff" 1906 assertDiff() 1907 1908 o.PortLabel = "diff" 1909 assertDiff() 1910 1911 o.AddressMode = AddressModeDriver 1912 assertDiff() 1913 1914 o.Tags = []string{"diff"} 1915 assertDiff() 1916 1917 o.CanaryTags = []string{"diff"} 1918 assertDiff() 1919 1920 o.Checks = []*ServiceCheck{{Name: "diff"}} 1921 assertDiff() 1922 1923 o.Connect = &ConsulConnect{Native: true} 1924 assertDiff() 1925 1926 o.EnableTagOverride = true 1927 assertDiff() 1928 1929 o.Provider = "nomad" 1930 assertDiff() 1931 1932 o.TaggedAddresses = map[string]string{"foo": "bar"} 1933 assertDiff() 1934 } 1935 1936 func TestService_validateNomadService(t *testing.T) { 1937 ci.Parallel(t) 1938 1939 testCases := []struct { 1940 inputService *Service 1941 inputErr *multierror.Error 1942 expectedOutputErrors []error 1943 name string 1944 }{ 1945 { 1946 inputService: &Service{ 1947 Name: "webapp", 1948 PortLabel: "http", 1949 Namespace: "default", 1950 Provider: "nomad", 1951 }, 1952 inputErr: &multierror.Error{}, 1953 expectedOutputErrors: nil, 1954 name: "valid service", 1955 }, 1956 { 1957 inputService: &Service{ 1958 Name: "webapp", 1959 PortLabel: "http", 1960 Namespace: "default", 1961 Provider: "nomad", 1962 Checks: []*ServiceCheck{{ 1963 Name: "webapp", 1964 Type: ServiceCheckHTTP, 1965 Method: "GET", 1966 Path: "/health", 1967 Interval: 3 * time.Second, 1968 Timeout: 1 * time.Second, 1969 }}, 1970 }, 1971 inputErr: &multierror.Error{}, 1972 expectedOutputErrors: nil, 1973 name: "valid service with checks", 1974 }, 1975 { 1976 inputService: &Service{ 1977 Name: "webapp", 1978 PortLabel: "http", 1979 Namespace: "default", 1980 Provider: "nomad", 1981 Connect: &ConsulConnect{ 1982 Native: true, 1983 }, 1984 }, 1985 inputErr: &multierror.Error{}, 1986 expectedOutputErrors: []error{errors.New("Service with provider nomad cannot include Connect blocks")}, 1987 name: "invalid service due to connect", 1988 }, 1989 { 1990 inputService: &Service{ 1991 Name: "webapp", 1992 PortLabel: "http", 1993 Namespace: "default", 1994 Provider: "nomad", 1995 Checks: []*ServiceCheck{ 1996 {Name: "some-check"}, 1997 }, 1998 }, 1999 inputErr: &multierror.Error{}, 2000 expectedOutputErrors: []error{ 2001 errors.New(`invalid check type (""), must be one of tcp, http`), 2002 }, 2003 name: "bad nomad check", 2004 }, 2005 } 2006 2007 for _, tc := range testCases { 2008 t.Run(tc.name, func(t *testing.T) { 2009 tc.inputService.validateNomadService(tc.inputErr) 2010 must.Eq(t, tc.expectedOutputErrors, tc.inputErr.Errors) 2011 }) 2012 } 2013 }