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