// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package agent import ( "fmt" "testing" "time" "github.com/hashicorp/consul/acl" "github.com/stretchr/testify/require" "github.com/hashicorp/consul/agent/structs" ) func TestAgent_sidecarServiceFromNodeService(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } tests := []struct { name string sd *structs.ServiceDefinition token string wantNS *structs.NodeService wantChecks []*structs.CheckType wantToken string wantErr string }{ { name: "no sidecar", sd: &structs.ServiceDefinition{ Name: "web", Port: 1111, }, token: "foo", wantNS: nil, wantChecks: nil, wantToken: "", wantErr: "", // Should NOT error }, { name: "all the defaults", sd: &structs.ServiceDefinition{ ID: "web1", Name: "web", Port: 1111, Connect: &structs.ServiceConnect{ SidecarService: &structs.ServiceDefinition{}, }, }, token: "foo", wantNS: &structs.NodeService{ EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), Kind: structs.ServiceKindConnectProxy, ID: "web1-sidecar-proxy", Service: "web-sidecar-proxy", Port: 0, LocallyRegisteredAsSidecar: true, Proxy: structs.ConnectProxyConfig{ DestinationServiceName: "web", DestinationServiceID: "web1", LocalServiceAddress: "127.0.0.1", LocalServicePort: 1111, }, }, wantChecks: nil, wantToken: "foo", }, { name: "all the allowed overrides", sd: &structs.ServiceDefinition{ ID: "web1", Name: "web", Port: 1111, Tags: []string{"baz"}, Meta: map[string]string{"foo": "baz"}, Connect: &structs.ServiceConnect{ SidecarService: &structs.ServiceDefinition{ Name: "motorbike1", Port: 3333, Tags: []string{"foo", "bar"}, Address: "127.127.127.127", Meta: map[string]string{"foo": "bar"}, Check: structs.CheckType{ ScriptArgs: []string{"sleep", "1"}, Interval: 999 * time.Second, }, Token: "custom-token", EnableTagOverride: true, Proxy: &structs.ConnectProxyConfig{ DestinationServiceName: "web", DestinationServiceID: "web1", LocalServiceAddress: "127.0.127.0", LocalServicePort: 9999, Config: map[string]interface{}{"baz": "qux"}, Upstreams: structs.TestUpstreams(t, false), }, }, }, }, token: "foo", wantNS: &structs.NodeService{ EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), Kind: structs.ServiceKindConnectProxy, ID: "web1-sidecar-proxy", Service: "motorbike1", Port: 3333, Tags: []string{"foo", "bar"}, Address: "127.127.127.127", Meta: map[string]string{ "foo": "bar", }, LocallyRegisteredAsSidecar: true, EnableTagOverride: true, Proxy: structs.ConnectProxyConfig{ DestinationServiceName: "web", DestinationServiceID: "web1", LocalServiceAddress: "127.0.127.0", LocalServicePort: 9999, Config: map[string]interface{}{"baz": "qux"}, Upstreams: structs.TestAddDefaultsToUpstreams(t, structs.TestUpstreams(t, false), *structs.DefaultEnterpriseMetaInDefaultPartition()), }, }, wantChecks: []*structs.CheckType{ { ScriptArgs: []string{"sleep", "1"}, Interval: 999 * time.Second, }, }, wantToken: "custom-token", }, { name: "inherit locality, tags and meta", sd: &structs.ServiceDefinition{ ID: "web1", Name: "web", Port: 1111, Tags: []string{"foo"}, Meta: map[string]string{"foo": "bar"}, Locality: &structs.Locality{ Region: "us-east-1", Zone: "us-east-1a", }, Connect: &structs.ServiceConnect{ SidecarService: &structs.ServiceDefinition{}, }, }, wantNS: &structs.NodeService{ EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), Kind: structs.ServiceKindConnectProxy, ID: "web1-sidecar-proxy", Service: "web-sidecar-proxy", Port: 0, Tags: []string{"foo"}, Meta: map[string]string{"foo": "bar"}, Locality: &structs.Locality{ Region: "us-east-1", Zone: "us-east-1a", }, LocallyRegisteredAsSidecar: true, Proxy: structs.ConnectProxyConfig{ DestinationServiceName: "web", DestinationServiceID: "web1", LocalServiceAddress: "127.0.0.1", LocalServicePort: 1111, }, }, wantChecks: nil, }, { name: "retain locality, tags and meta if explicitly configured", sd: &structs.ServiceDefinition{ ID: "web1", Name: "web", Port: 1111, Tags: []string{"foo"}, Meta: map[string]string{"foo": "bar"}, Locality: &structs.Locality{ Region: "us-east-1", Zone: "us-east-1a", }, Connect: &structs.ServiceConnect{ SidecarService: &structs.ServiceDefinition{ Tags: []string{"bar"}, Meta: map[string]string{"baz": "qux"}, Locality: &structs.Locality{ Region: "us-east-2", Zone: "us-east-2a", }, }, }, }, wantNS: &structs.NodeService{ EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), Kind: structs.ServiceKindConnectProxy, ID: "web1-sidecar-proxy", Service: "web-sidecar-proxy", Port: 0, Tags: []string{"bar"}, Meta: map[string]string{"baz": "qux"}, Locality: &structs.Locality{ Region: "us-east-2", Zone: "us-east-2a", }, LocallyRegisteredAsSidecar: true, Proxy: structs.ConnectProxyConfig{ DestinationServiceName: "web", DestinationServiceID: "web1", LocalServiceAddress: "127.0.0.1", LocalServicePort: 1111, }, }, wantChecks: nil, }, { name: "invalid check type", sd: &structs.ServiceDefinition{ ID: "web1", Name: "web", Port: 1111, Connect: &structs.ServiceConnect{ SidecarService: &structs.ServiceDefinition{ Check: structs.CheckType{ TCP: "foo", // Invalid since no interval specified }, }, }, }, token: "foo", wantErr: "Interval must be > 0", }, { name: "invalid meta", sd: &structs.ServiceDefinition{ ID: "web1", Name: "web", Port: 1111, Connect: &structs.ServiceConnect{ SidecarService: &structs.ServiceDefinition{ Meta: map[string]string{ "consul-reserved-key-should-be-rejected": "true", }, }, }, }, token: "foo", wantErr: "reserved for internal use", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ns := tt.sd.NodeService() err := ns.Validate() require.NoError(t, err, "Invalid test case - NodeService must validate") gotNS, gotChecks, gotToken, err := sidecarServiceFromNodeService(ns, tt.token) if tt.wantErr != "" { require.Error(t, err) require.Contains(t, err.Error(), tt.wantErr) return } require.NoError(t, err) require.Equal(t, tt.wantNS, gotNS) require.Equal(t, tt.wantChecks, gotChecks) require.Equal(t, tt.wantToken, gotToken) }) } } func TestAgent_SidecarPortFromServiceID(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } tests := []struct { name string autoPortsDisabled bool enterpriseMeta acl.EnterpriseMeta maxPort int port int preRegister []*structs.ServiceDefinition serviceID string wantPort int wantErr string }{ { name: "use auto ports", serviceID: "web1", wantPort: 2222, }, { name: "re-registering same sidecar with no port should pick same one", // Allow multiple ports to be sure we get the right one maxPort: 2500, // Pre register the main service and sidecar we want preRegister: []*structs.ServiceDefinition{{ Kind: structs.ServiceKindConnectProxy, ID: "web1", EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), Name: "web", Port: 2221, Proxy: &structs.ConnectProxyConfig{ DestinationServiceName: "web", DestinationServiceID: "web1", LocalServiceAddress: "127.0.0.1", LocalServicePort: 1110, }, }, { Kind: structs.ServiceKindConnectProxy, ID: "web1-sidecar-proxy", EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), Name: "web-sidecar-proxy", Port: 2222, Proxy: &structs.ConnectProxyConfig{ DestinationServiceName: "web", DestinationServiceID: "web1", LocalServiceAddress: "127.0.0.1", LocalServicePort: 1111, }, }}, // Register same sidecar again serviceID: "web1-sidecar-proxy", enterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), wantPort: 2222, // Should claim the same port as before }, { name: "all auto ports already taken", // register another service with sidecar consuming our 1 and only allocated auto port. preRegister: []*structs.ServiceDefinition{{ Kind: structs.ServiceKindConnectProxy, ID: "api1", EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), Name: "api", Port: 2221, Proxy: &structs.ConnectProxyConfig{ DestinationServiceName: "api", DestinationServiceID: "api1", LocalServiceAddress: "127.0.0.1", LocalServicePort: 1110, }, }, { Kind: structs.ServiceKindConnectProxy, Name: "api-proxy-sidecar", Port: 2222, // Consume the one available auto-port Proxy: &structs.ConnectProxyConfig{ DestinationServiceID: "api1", DestinationServiceName: "api", }, }}, wantErr: "none left in the configured range [2222, 2222]", }, { name: "auto ports disabled", autoPortsDisabled: true, wantErr: "auto-assignment disabled in config", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Set port range to be tiny (one available) to test consuming all of it. // This allows a single assigned port at 2222 thanks to being inclusive at // both ends. if tt.maxPort == 0 { tt.maxPort = 2222 } hcl := fmt.Sprintf(` ports { sidecar_min_port = 2222 sidecar_max_port = %d } `, tt.maxPort) if tt.autoPortsDisabled { hcl = ` ports { sidecar_min_port = 0 sidecar_max_port = 0 } ` } a := NewTestAgent(t, hcl) defer a.Shutdown() if len(tt.preRegister) > 0 { for _, s := range tt.preRegister { err := a.addServiceFromSource(s.NodeService(), nil, false, "", ConfigSourceLocal) require.NoError(t, err) } } gotPort, err := a.sidecarPortFromServiceIDLocked(structs.ServiceID{ID: tt.serviceID, EnterpriseMeta: tt.enterpriseMeta}) if tt.wantErr != "" { require.Error(t, err) require.Contains(t, err.Error(), tt.wantErr) return } require.NoError(t, err) require.Equal(t, tt.wantPort, gotPort) }) } } func TestAgent_SidecarDefaultChecks(t *testing.T) { tests := []struct { name string serviceID string svcAddress string proxyLocalSvcAddress string port int wantChecks []*structs.CheckType }{{ name: "uses proxy address for check", serviceID: "web1-1-sidecar-proxy", svcAddress: "123.123.123.123", proxyLocalSvcAddress: "255.255.255.255", port: 2222, wantChecks: []*structs.CheckType{ { Name: "Connect Sidecar Listening", TCP: "123.123.123.123:2222", Interval: 10 * time.Second, }, { Name: "Connect Sidecar Aliasing web1-1", AliasService: "web1-1", }, }, }, { name: "uses proxy.local_service_address for check if proxy address is empty", serviceID: "web1-1-sidecar-proxy", proxyLocalSvcAddress: "1.2.3.4", port: 2222, wantChecks: []*structs.CheckType{ { Name: "Connect Sidecar Listening", TCP: "1.2.3.4:2222", Interval: 10 * time.Second, }, { Name: "Connect Sidecar Aliasing web1-1", AliasService: "web1-1", }, }, }, { name: "redundant name", serviceID: "1-sidecar-proxy-web1-sidecar-proxy", svcAddress: "123.123.123.123", proxyLocalSvcAddress: "255.255.255.255", port: 2222, wantChecks: []*structs.CheckType{ { Name: "Connect Sidecar Listening", TCP: "123.123.123.123:2222", Interval: 10 * time.Second, }, { Name: "Connect Sidecar Aliasing 1-sidecar-proxy-web1", AliasService: "1-sidecar-proxy-web1", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotChecks := sidecarDefaultChecks(tt.serviceID, tt.svcAddress, tt.proxyLocalSvcAddress, tt.port) require.Equal(t, tt.wantChecks, gotChecks) }) } }