github.com/hernad/nomad@v1.6.112/command/agent/consul/connect_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package consul 5 6 import ( 7 "testing" 8 "time" 9 10 "github.com/hashicorp/consul/api" 11 "github.com/hernad/nomad/ci" 12 "github.com/hernad/nomad/helper/pointer" 13 "github.com/hernad/nomad/helper/uuid" 14 "github.com/hernad/nomad/nomad/structs" 15 "github.com/shoenig/test/must" 16 "github.com/stretchr/testify/require" 17 ) 18 19 var ( 20 testConnectNetwork = structs.Networks{{ 21 Mode: "bridge", 22 Device: "eth0", 23 IP: "192.168.30.1", 24 DynamicPorts: []structs.Port{ 25 {Label: "healthPort", Value: 23100, To: 23100}, 26 {Label: "metricsPort", Value: 23200, To: 23200}, 27 {Label: "connect-proxy-redis", Value: 3000, To: 3000}, 28 }, 29 }} 30 testConnectPorts = structs.AllocatedPorts{{ 31 Label: "connect-proxy-redis", 32 Value: 3000, 33 To: 3000, 34 HostIP: "192.168.30.1", 35 }} 36 ) 37 38 func TestConnect_newConnect(t *testing.T) { 39 ci.Parallel(t) 40 41 service := "redis" 42 redisID := uuid.Generate() 43 allocID := uuid.Generate() 44 info := structs.AllocInfo{ 45 AllocID: allocID, 46 } 47 48 t.Run("nil", func(t *testing.T) { 49 asr, err := newConnect("", structs.AllocInfo{}, "", nil, nil, nil) 50 require.NoError(t, err) 51 require.Nil(t, asr) 52 }) 53 54 t.Run("native", func(t *testing.T) { 55 asr, err := newConnect(redisID, info, service, &structs.ConsulConnect{ 56 Native: true, 57 }, nil, nil) 58 require.NoError(t, err) 59 require.True(t, asr.Native) 60 require.Nil(t, asr.SidecarService) 61 }) 62 63 t.Run("with sidecar", func(t *testing.T) { 64 asr, err := newConnect(redisID, info, service, &structs.ConsulConnect{ 65 Native: false, 66 SidecarService: &structs.ConsulSidecarService{ 67 Tags: []string{"foo", "bar"}, 68 Port: "connect-proxy-redis", 69 }, 70 }, testConnectNetwork, testConnectPorts) 71 require.NoError(t, err) 72 require.Equal(t, &api.AgentServiceRegistration{ 73 Tags: []string{"foo", "bar"}, 74 Port: 3000, 75 Address: "192.168.30.1", 76 Proxy: &api.AgentServiceConnectProxyConfig{ 77 Config: map[string]interface{}{ 78 "bind_address": "0.0.0.0", 79 "bind_port": 3000, 80 "envoy_stats_tags": []string{"nomad.alloc_id=" + allocID}, 81 }, 82 }, 83 Checks: api.AgentServiceChecks{ 84 { 85 Name: "Connect Sidecar Aliasing " + redisID, 86 AliasService: redisID, 87 }, 88 { 89 Name: "Connect Sidecar Listening", 90 TCP: "192.168.30.1:3000", 91 Interval: "10s", 92 }, 93 }, 94 }, asr.SidecarService) 95 }) 96 97 t.Run("with sidecar without TCP checks", func(t *testing.T) { 98 asr, err := newConnect(redisID, info, service, &structs.ConsulConnect{ 99 Native: false, 100 SidecarService: &structs.ConsulSidecarService{ 101 Tags: []string{"foo", "bar"}, 102 Port: "connect-proxy-redis", 103 DisableDefaultTCPCheck: true, 104 }, 105 }, testConnectNetwork, testConnectPorts) 106 require.NoError(t, err) 107 require.Equal(t, &api.AgentServiceRegistration{ 108 Tags: []string{"foo", "bar"}, 109 Port: 3000, 110 Address: "192.168.30.1", 111 Proxy: &api.AgentServiceConnectProxyConfig{ 112 Config: map[string]interface{}{ 113 "bind_address": "0.0.0.0", 114 "bind_port": 3000, 115 "envoy_stats_tags": []string{"nomad.alloc_id=" + allocID}, 116 }, 117 }, 118 Checks: api.AgentServiceChecks{ 119 { 120 Name: "Connect Sidecar Aliasing " + redisID, 121 AliasService: redisID, 122 }, 123 }, 124 }, asr.SidecarService) 125 }) 126 } 127 128 func TestConnect_connectSidecarRegistration(t *testing.T) { 129 ci.Parallel(t) 130 131 redisID := uuid.Generate() 132 allocID := uuid.Generate() 133 info := structs.AllocInfo{ 134 AllocID: allocID, 135 } 136 137 t.Run("nil", func(t *testing.T) { 138 sidecarReg, err := connectSidecarRegistration(redisID, info, nil, testConnectNetwork, testConnectPorts) 139 require.NoError(t, err) 140 require.Nil(t, sidecarReg) 141 }) 142 143 t.Run("no service port", func(t *testing.T) { 144 _, err := connectSidecarRegistration("unknown-id", info, &structs.ConsulSidecarService{ 145 Port: "unknown-label", 146 }, testConnectNetwork, testConnectPorts) 147 require.EqualError(t, err, `No port of label "unknown-label" defined`) 148 }) 149 150 t.Run("bad proxy", func(t *testing.T) { 151 _, err := connectSidecarRegistration(redisID, info, &structs.ConsulSidecarService{ 152 Port: "connect-proxy-redis", 153 Proxy: &structs.ConsulProxy{ 154 Expose: &structs.ConsulExposeConfig{ 155 Paths: []structs.ConsulExposePath{{ 156 ListenerPort: "badPort", 157 }}, 158 }, 159 }, 160 }, testConnectNetwork, testConnectPorts) 161 require.EqualError(t, err, `No port of label "badPort" defined`) 162 }) 163 164 t.Run("normal", func(t *testing.T) { 165 proxy, err := connectSidecarRegistration(redisID, info, &structs.ConsulSidecarService{ 166 Tags: []string{"foo", "bar"}, 167 Port: "connect-proxy-redis", 168 }, testConnectNetwork, testConnectPorts) 169 require.NoError(t, err) 170 require.Equal(t, &api.AgentServiceRegistration{ 171 Tags: []string{"foo", "bar"}, 172 Port: 3000, 173 Address: "192.168.30.1", 174 Proxy: &api.AgentServiceConnectProxyConfig{ 175 Config: map[string]interface{}{ 176 "bind_address": "0.0.0.0", 177 "bind_port": 3000, 178 "envoy_stats_tags": []string{"nomad.alloc_id=" + allocID}, 179 }, 180 }, 181 Checks: api.AgentServiceChecks{ 182 { 183 Name: "Connect Sidecar Aliasing " + redisID, 184 AliasService: redisID, 185 }, 186 { 187 Name: "Connect Sidecar Listening", 188 TCP: "192.168.30.1:3000", 189 Interval: "10s", 190 }, 191 }, 192 }, proxy) 193 }) 194 } 195 196 func TestConnect_connectProxy(t *testing.T) { 197 ci.Parallel(t) 198 199 allocID := uuid.Generate() 200 info := structs.AllocInfo{ 201 AllocID: allocID, 202 } 203 204 // If the input proxy is nil, we expect the output to be a proxy with its 205 // config set to default values. 206 t.Run("nil proxy", func(t *testing.T) { 207 proxy, err := connectSidecarProxy(info, nil, 2000, testConnectNetwork) 208 require.NoError(t, err) 209 require.Equal(t, &api.AgentServiceConnectProxyConfig{ 210 LocalServiceAddress: "", 211 LocalServicePort: 0, 212 Upstreams: nil, 213 Expose: api.ExposeConfig{}, 214 Config: map[string]interface{}{ 215 "bind_address": "0.0.0.0", 216 "bind_port": 2000, 217 "envoy_stats_tags": []string{"nomad.alloc_id=" + allocID}, 218 }, 219 }, proxy) 220 }) 221 222 t.Run("bad proxy", func(t *testing.T) { 223 _, err := connectSidecarProxy(info, &structs.ConsulProxy{ 224 LocalServiceAddress: "0.0.0.0", 225 LocalServicePort: 2000, 226 Upstreams: nil, 227 Expose: &structs.ConsulExposeConfig{ 228 Paths: []structs.ConsulExposePath{{ 229 ListenerPort: "badPort", 230 }}, 231 }, 232 Config: nil, 233 }, 2000, testConnectNetwork) 234 require.EqualError(t, err, `No port of label "badPort" defined`) 235 }) 236 237 t.Run("normal", func(t *testing.T) { 238 proxy, err := connectSidecarProxy(info, &structs.ConsulProxy{ 239 LocalServiceAddress: "0.0.0.0", 240 LocalServicePort: 2000, 241 Upstreams: nil, 242 Expose: &structs.ConsulExposeConfig{ 243 Paths: []structs.ConsulExposePath{{ 244 Path: "/health", 245 Protocol: "http", 246 LocalPathPort: 8000, 247 ListenerPort: "healthPort", 248 }}, 249 }, 250 Config: nil, 251 }, 2000, testConnectNetwork) 252 require.NoError(t, err) 253 require.Equal(t, &api.AgentServiceConnectProxyConfig{ 254 LocalServiceAddress: "0.0.0.0", 255 LocalServicePort: 2000, 256 Upstreams: nil, 257 Expose: api.ExposeConfig{ 258 Paths: []api.ExposePath{{ 259 Path: "/health", 260 Protocol: "http", 261 LocalPathPort: 8000, 262 ListenerPort: 23100, 263 }}, 264 }, 265 Config: map[string]interface{}{ 266 "bind_address": "0.0.0.0", 267 "bind_port": 2000, 268 "envoy_stats_tags": []string{"nomad.alloc_id=" + allocID}, 269 }, 270 }, proxy) 271 }) 272 } 273 274 func TestConnect_connectProxyExpose(t *testing.T) { 275 ci.Parallel(t) 276 277 t.Run("nil", func(t *testing.T) { 278 exposeConfig, err := connectProxyExpose(nil, nil) 279 require.NoError(t, err) 280 require.Equal(t, api.ExposeConfig{}, exposeConfig) 281 }) 282 283 t.Run("bad port", func(t *testing.T) { 284 _, err := connectProxyExpose(&structs.ConsulExposeConfig{ 285 Paths: []structs.ConsulExposePath{{ 286 ListenerPort: "badPort", 287 }}, 288 }, testConnectNetwork) 289 require.EqualError(t, err, `No port of label "badPort" defined`) 290 }) 291 292 t.Run("normal", func(t *testing.T) { 293 expose, err := connectProxyExpose(&structs.ConsulExposeConfig{ 294 Paths: []structs.ConsulExposePath{{ 295 Path: "/health", 296 Protocol: "http", 297 LocalPathPort: 8000, 298 ListenerPort: "healthPort", 299 }}, 300 }, testConnectNetwork) 301 require.NoError(t, err) 302 require.Equal(t, api.ExposeConfig{ 303 Checks: false, 304 Paths: []api.ExposePath{{ 305 Path: "/health", 306 ListenerPort: 23100, 307 LocalPathPort: 8000, 308 Protocol: "http", 309 ParsedFromCheck: false, 310 }}, 311 }, expose) 312 }) 313 } 314 315 func TestConnect_connectProxyExposePaths(t *testing.T) { 316 ci.Parallel(t) 317 318 t.Run("nil", func(t *testing.T) { 319 upstreams, err := connectProxyExposePaths(nil, nil) 320 require.NoError(t, err) 321 require.Empty(t, upstreams) 322 }) 323 324 t.Run("no network", func(t *testing.T) { 325 original := []structs.ConsulExposePath{{Path: "/path"}} 326 _, err := connectProxyExposePaths(original, nil) 327 require.EqualError(t, err, `Connect only supported with exactly 1 network (found 0)`) 328 }) 329 330 t.Run("normal", func(t *testing.T) { 331 original := []structs.ConsulExposePath{{ 332 Path: "/health", 333 Protocol: "http", 334 LocalPathPort: 8000, 335 ListenerPort: "healthPort", 336 }, { 337 Path: "/metrics", 338 Protocol: "grpc", 339 LocalPathPort: 9500, 340 ListenerPort: "metricsPort", 341 }} 342 exposePaths, err := connectProxyExposePaths(original, testConnectNetwork) 343 require.NoError(t, err) 344 require.Equal(t, []api.ExposePath{ 345 { 346 Path: "/health", 347 Protocol: "http", 348 LocalPathPort: 8000, 349 ListenerPort: 23100, 350 ParsedFromCheck: false, 351 }, 352 { 353 Path: "/metrics", 354 Protocol: "grpc", 355 LocalPathPort: 9500, 356 ListenerPort: 23200, 357 ParsedFromCheck: false, 358 }, 359 }, exposePaths) 360 }) 361 } 362 363 func TestConnect_connectUpstreams(t *testing.T) { 364 ci.Parallel(t) 365 366 t.Run("nil", func(t *testing.T) { 367 must.Nil(t, connectUpstreams(nil)) 368 }) 369 370 t.Run("not empty", func(t *testing.T) { 371 must.Eq(t, 372 []api.Upstream{{ 373 DestinationName: "foo", 374 LocalBindPort: 8000, 375 }, { 376 DestinationName: "bar", 377 DestinationNamespace: "ns2", 378 LocalBindPort: 9000, 379 Datacenter: "dc2", 380 LocalBindAddress: "127.0.0.2", 381 Config: map[string]any{"connect_timeout_ms": 5000}, 382 }}, 383 connectUpstreams([]structs.ConsulUpstream{{ 384 DestinationName: "foo", 385 LocalBindPort: 8000, 386 }, { 387 DestinationName: "bar", 388 DestinationNamespace: "ns2", 389 LocalBindPort: 9000, 390 Datacenter: "dc2", 391 LocalBindAddress: "127.0.0.2", 392 Config: map[string]any{"connect_timeout_ms": 5000}, 393 }}), 394 ) 395 }) 396 } 397 398 func TestConnect_connectProxyConfig(t *testing.T) { 399 ci.Parallel(t) 400 401 t.Run("nil map", func(t *testing.T) { 402 require.Equal(t, map[string]interface{}{ 403 "bind_address": "0.0.0.0", 404 "bind_port": 42, 405 "envoy_stats_tags": []string{"nomad.alloc_id=test_alloc1"}, 406 }, connectProxyConfig(nil, 42, structs.AllocInfo{AllocID: "test_alloc1"})) 407 }) 408 409 t.Run("pre-existing map", func(t *testing.T) { 410 require.Equal(t, map[string]interface{}{ 411 "bind_address": "0.0.0.0", 412 "bind_port": 42, 413 "foo": "bar", 414 "envoy_stats_tags": []string{"nomad.alloc_id=test_alloc2"}, 415 }, connectProxyConfig(map[string]interface{}{ 416 "foo": "bar", 417 }, 42, structs.AllocInfo{AllocID: "test_alloc2"})) 418 }) 419 } 420 421 func TestConnect_getConnectPort(t *testing.T) { 422 ci.Parallel(t) 423 424 networks := structs.Networks{{ 425 IP: "192.168.30.1", 426 DynamicPorts: []structs.Port{{ 427 Label: "connect-proxy-foo", 428 Value: 23456, 429 To: 23456, 430 }}}} 431 432 ports := structs.AllocatedPorts{{ 433 Label: "foo", 434 Value: 23456, 435 To: 23456, 436 HostIP: "192.168.30.1", 437 }} 438 439 t.Run("normal", func(t *testing.T) { 440 nr, err := connectPort("foo", networks, ports) 441 require.NoError(t, err) 442 require.Equal(t, structs.AllocatedPortMapping{ 443 Label: "foo", 444 Value: 23456, 445 To: 23456, 446 HostIP: "192.168.30.1", 447 }, nr) 448 }) 449 450 t.Run("no such service", func(t *testing.T) { 451 _, err := connectPort("other", networks, ports) 452 require.EqualError(t, err, `No port of label "other" defined`) 453 }) 454 455 t.Run("no network", func(t *testing.T) { 456 _, err := connectPort("foo", nil, nil) 457 require.EqualError(t, err, "Connect only supported with exactly 1 network (found 0)") 458 }) 459 460 t.Run("multi network", func(t *testing.T) { 461 _, err := connectPort("foo", append(networks, &structs.NetworkResource{ 462 Device: "eth1", 463 IP: "10.0.10.0", 464 }), nil) 465 require.EqualError(t, err, "Connect only supported with exactly 1 network (found 2)") 466 }) 467 } 468 469 func TestConnect_getExposePathPort(t *testing.T) { 470 ci.Parallel(t) 471 472 networks := structs.Networks{{ 473 Device: "eth0", 474 IP: "192.168.30.1", 475 DynamicPorts: []structs.Port{{ 476 Label: "myPort", 477 Value: 23456, 478 To: 23456, 479 }}}} 480 481 t.Run("normal", func(t *testing.T) { 482 ip, port, err := connectExposePathPort("myPort", networks) 483 require.NoError(t, err) 484 require.Equal(t, ip, "192.168.30.1") 485 require.Equal(t, 23456, port) 486 }) 487 488 t.Run("no such port label", func(t *testing.T) { 489 _, _, err := connectExposePathPort("otherPort", networks) 490 require.EqualError(t, err, `No port of label "otherPort" defined`) 491 }) 492 493 t.Run("no network", func(t *testing.T) { 494 _, _, err := connectExposePathPort("myPort", nil) 495 require.EqualError(t, err, "Connect only supported with exactly 1 network (found 0)") 496 }) 497 498 t.Run("multi network", func(t *testing.T) { 499 _, _, err := connectExposePathPort("myPort", append(networks, &structs.NetworkResource{ 500 Device: "eth1", 501 IP: "10.0.10.0", 502 })) 503 require.EqualError(t, err, "Connect only supported with exactly 1 network (found 2)") 504 }) 505 } 506 507 func TestConnect_newConnectGateway(t *testing.T) { 508 ci.Parallel(t) 509 510 t.Run("not a gateway", func(t *testing.T) { 511 result := newConnectGateway(&structs.ConsulConnect{Native: true}) 512 require.Nil(t, result) 513 }) 514 515 t.Run("canonical empty", func(t *testing.T) { 516 result := newConnectGateway(&structs.ConsulConnect{ 517 Gateway: &structs.ConsulGateway{ 518 Proxy: &structs.ConsulGatewayProxy{ 519 ConnectTimeout: pointer.Of(1 * time.Second), 520 EnvoyGatewayBindTaggedAddresses: false, 521 EnvoyGatewayBindAddresses: nil, 522 EnvoyGatewayNoDefaultBind: false, 523 Config: nil, 524 }, 525 }, 526 }) 527 require.Equal(t, &api.AgentServiceConnectProxyConfig{ 528 Config: map[string]interface{}{ 529 "connect_timeout_ms": int64(1000), 530 }, 531 }, result) 532 }) 533 534 t.Run("proxy undefined", func(t *testing.T) { 535 result := newConnectGateway(&structs.ConsulConnect{ 536 Gateway: &structs.ConsulGateway{ 537 Proxy: nil, 538 }, 539 }) 540 require.Equal(t, &api.AgentServiceConnectProxyConfig{ 541 Config: nil, 542 }, result) 543 }) 544 545 t.Run("full", func(t *testing.T) { 546 result := newConnectGateway(&structs.ConsulConnect{ 547 Gateway: &structs.ConsulGateway{ 548 Proxy: &structs.ConsulGatewayProxy{ 549 ConnectTimeout: pointer.Of(1 * time.Second), 550 EnvoyGatewayBindTaggedAddresses: true, 551 EnvoyGatewayBindAddresses: map[string]*structs.ConsulGatewayBindAddress{ 552 "service1": { 553 Address: "10.0.0.1", 554 Port: 2000, 555 }, 556 }, 557 EnvoyGatewayNoDefaultBind: true, 558 EnvoyDNSDiscoveryType: "STRICT_DNS", 559 Config: map[string]interface{}{ 560 "foo": 1, 561 }, 562 }, 563 }, 564 }) 565 require.Equal(t, &api.AgentServiceConnectProxyConfig{ 566 Config: map[string]interface{}{ 567 "connect_timeout_ms": int64(1000), 568 "envoy_gateway_bind_tagged_addresses": true, 569 "envoy_gateway_bind_addresses": map[string]*structs.ConsulGatewayBindAddress{ 570 "service1": { 571 Address: "10.0.0.1", 572 Port: 2000, 573 }, 574 }, 575 "envoy_gateway_no_default_bind": true, 576 "envoy_dns_discovery_type": "STRICT_DNS", 577 "foo": 1, 578 }, 579 }, result) 580 }) 581 } 582 583 func Test_connectMeshGateway(t *testing.T) { 584 ci.Parallel(t) 585 586 t.Run("empty", func(t *testing.T) { 587 result := connectMeshGateway(structs.ConsulMeshGateway{}) 588 require.Equal(t, api.MeshGatewayConfig{Mode: api.MeshGatewayModeDefault}, result) 589 }) 590 591 t.Run("local", func(t *testing.T) { 592 result := connectMeshGateway(structs.ConsulMeshGateway{Mode: "local"}) 593 require.Equal(t, api.MeshGatewayConfig{Mode: api.MeshGatewayModeLocal}, result) 594 }) 595 596 t.Run("remote", func(t *testing.T) { 597 result := connectMeshGateway(structs.ConsulMeshGateway{Mode: "remote"}) 598 require.Equal(t, api.MeshGatewayConfig{Mode: api.MeshGatewayModeRemote}, result) 599 }) 600 601 t.Run("none", func(t *testing.T) { 602 result := connectMeshGateway(structs.ConsulMeshGateway{Mode: "none"}) 603 require.Equal(t, api.MeshGatewayConfig{Mode: api.MeshGatewayModeNone}, result) 604 }) 605 606 t.Run("nonsense", func(t *testing.T) { 607 result := connectMeshGateway(structs.ConsulMeshGateway{}) 608 require.Equal(t, api.MeshGatewayConfig{Mode: api.MeshGatewayModeDefault}, result) 609 }) 610 } 611 612 func Test_injectNomadInfo(t *testing.T) { 613 ci.Parallel(t) 614 615 info1 := func() map[string]string { 616 return map[string]string{ 617 "nomad.alloc_id=": "abc123", 618 } 619 } 620 info2 := func() map[string]string { 621 return map[string]string{ 622 "nomad.alloc_id=": "abc123", 623 "nomad.namespace=": "testns", 624 } 625 } 626 627 try := func(defaultTags map[string]string, cfg, exp map[string]interface{}) { 628 // TODO: defaultTags get modified over the execution 629 injectNomadInfo(cfg, defaultTags) 630 cfgTags, expTags := cfg["envoy_stats_tags"], exp["envoy_stats_tags"] 631 delete(cfg, "envoy_stats_tags") 632 delete(exp, "envoy_stats_tags") 633 require.Equal(t, exp, cfg) 634 require.ElementsMatch(t, expTags, cfgTags, "") 635 } 636 637 // empty 638 try( 639 info1(), 640 make(map[string]interface{}), 641 map[string]interface{}{ 642 "envoy_stats_tags": []string{"nomad.alloc_id=abc123"}, 643 }, 644 ) 645 646 // merge fresh 647 try( 648 info1(), 649 map[string]interface{}{"foo": "bar"}, 650 map[string]interface{}{ 651 "foo": "bar", 652 "envoy_stats_tags": []string{"nomad.alloc_id=abc123"}, 653 }, 654 ) 655 656 // merge append 657 try( 658 info1(), 659 map[string]interface{}{ 660 "foo": "bar", 661 "envoy_stats_tags": []string{"k1=v1", "k2=v2"}, 662 }, 663 map[string]interface{}{ 664 "foo": "bar", 665 "envoy_stats_tags": []string{"k1=v1", "k2=v2", "nomad.alloc_id=abc123"}, 666 }, 667 ) 668 669 // merge exists 670 try( 671 info2(), 672 map[string]interface{}{ 673 "foo": "bar", 674 "envoy_stats_tags": []string{"k1=v1", "k2=v2", "nomad.alloc_id=xyz789"}, 675 }, 676 map[string]interface{}{ 677 "foo": "bar", 678 "envoy_stats_tags": []string{"k1=v1", "k2=v2", "nomad.alloc_id=xyz789", "nomad.namespace=testns"}, 679 }, 680 ) 681 682 // merge wrong type 683 try( 684 info1(), 685 map[string]interface{}{ 686 "envoy_stats_tags": "not a slice of string", 687 }, 688 map[string]interface{}{ 689 "envoy_stats_tags": []string{"nomad.alloc_id=abc123"}, 690 }, 691 ) 692 }