Merge pull request #14056 from hashicorp/proxy-register-port-race

Refactor sidecar_service method to separate port assignment
This commit is contained in:
skpratt 2022-08-10 09:46:29 -05:00 committed by GitHub
commit 79c23a7cd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 134 additions and 120 deletions

View File

@ -114,9 +114,35 @@ func (a *Agent) sidecarServiceFromNodeService(ns *structs.NodeService, token str
} }
} }
if sidecar.Port < 1 {
port, err := a.sidecarPortFromServiceID(sidecar.CompoundServiceID())
if err != nil {
return nil, nil, "", err
}
sidecar.Port = port
}
// Setup checks
checks, err := ns.Connect.SidecarService.CheckTypes()
if err != nil {
return nil, nil, "", err
}
// Setup default check if none given
if len(checks) < 1 {
checks = sidecarDefaultChecks(ns.ID, sidecar.Proxy.LocalServiceAddress, sidecar.Port)
}
return sidecar, checks, token, nil
}
// sidecarPortFromServiceID is used to allocate a unique port for a sidecar proxy.
// This is called immediately before registration to avoid value collisions. This function assumes the state lock is already held.
func (a *Agent) sidecarPortFromServiceID(sidecarCompoundServiceID structs.ServiceID) (int, error) {
sidecarPort := 0
// Allocate port if needed (min and max inclusive). // Allocate port if needed (min and max inclusive).
rangeLen := a.config.ConnectSidecarMaxPort - a.config.ConnectSidecarMinPort + 1 rangeLen := a.config.ConnectSidecarMaxPort - a.config.ConnectSidecarMinPort + 1
if sidecar.Port < 1 && a.config.ConnectSidecarMinPort > 0 && rangeLen > 0 { if sidecarPort < 1 && a.config.ConnectSidecarMinPort > 0 && rangeLen > 0 {
// This did pick at random which was simpler but consul reload would assign // This did pick at random which was simpler but consul reload would assign
// new ports to all the sidecars since it unloads all state and // new ports to all the sidecars since it unloads all state and
// re-populates. It also made this more difficult to test (have to pin the // re-populates. It also made this more difficult to test (have to pin the
@ -130,11 +156,11 @@ func (a *Agent) sidecarServiceFromNodeService(ns *structs.NodeService, token str
// Check if other port is in auto-assign range // Check if other port is in auto-assign range
if otherNS.Port >= a.config.ConnectSidecarMinPort && if otherNS.Port >= a.config.ConnectSidecarMinPort &&
otherNS.Port <= a.config.ConnectSidecarMaxPort { otherNS.Port <= a.config.ConnectSidecarMaxPort {
if otherNS.CompoundServiceID() == sidecar.CompoundServiceID() { if otherNS.CompoundServiceID() == sidecarCompoundServiceID {
// This sidecar is already registered with an auto-port and is just // This sidecar is already registered with an auto-port and is just
// being updated so pick the same port as before rather than allocate // being updated so pick the same port as before rather than allocate
// a new one. // a new one.
sidecar.Port = otherNS.Port sidecarPort = otherNS.Port
break break
} }
usedPorts[otherNS.Port] = struct{}{} usedPorts[otherNS.Port] = struct{}{}
@ -147,54 +173,48 @@ func (a *Agent) sidecarServiceFromNodeService(ns *structs.NodeService, token str
// Check we still need to assign a port and didn't find we already had one // Check we still need to assign a port and didn't find we already had one
// allocated. // allocated.
if sidecar.Port < 1 { if sidecarPort < 1 {
// Iterate until we find lowest unused port // Iterate until we find lowest unused port
for p := a.config.ConnectSidecarMinPort; p <= a.config.ConnectSidecarMaxPort; p++ { for p := a.config.ConnectSidecarMinPort; p <= a.config.ConnectSidecarMaxPort; p++ {
_, used := usedPorts[p] _, used := usedPorts[p]
if !used { if !used {
sidecar.Port = p sidecarPort = p
break break
} }
} }
} }
} }
// If no ports left (or auto ports disabled) fail // If no ports left (or auto ports disabled) fail
if sidecar.Port < 1 { if sidecarPort < 1 {
// If ports are set to zero explicitly, config builder switches them to // If ports are set to zero explicitly, config builder switches them to
// `-1`. In this case don't show the actual values since we don't know what // `-1`. In this case don't show the actual values since we don't know what
// was actually in config (zero or negative) and it might be confusing, we // was actually in config (zero or negative) and it might be confusing, we
// just know they explicitly disabled auto assignment. // just know they explicitly disabled auto assignment.
if a.config.ConnectSidecarMinPort < 1 || a.config.ConnectSidecarMaxPort < 1 { if a.config.ConnectSidecarMinPort < 1 || a.config.ConnectSidecarMaxPort < 1 {
return nil, nil, "", fmt.Errorf("no port provided for sidecar_service " + return 0, fmt.Errorf("no port provided for sidecar_service " +
"and auto-assignment disabled in config") "and auto-assignment disabled in config")
} }
return nil, nil, "", fmt.Errorf("no port provided for sidecar_service and none "+ return 0, fmt.Errorf("no port provided for sidecar_service and none "+
"left in the configured range [%d, %d]", a.config.ConnectSidecarMinPort, "left in the configured range [%d, %d]", a.config.ConnectSidecarMinPort,
a.config.ConnectSidecarMaxPort) a.config.ConnectSidecarMaxPort)
} }
// Setup checks return sidecarPort, nil
checks, err := ns.Connect.SidecarService.CheckTypes() }
if err != nil {
return nil, nil, "", err func sidecarDefaultChecks(serviceID string, localServiceAddress string, port int) []*structs.CheckType {
} // Setup default check if none given
return []*structs.CheckType{
// Setup default check if none given {
if len(checks) < 1 { Name: "Connect Sidecar Listening",
checks = []*structs.CheckType{ // Default to localhost rather than agent/service public IP. The checks
{ // can always be overridden if a non-loopback IP is needed.
Name: "Connect Sidecar Listening", TCP: ipaddr.FormatAddressPort(localServiceAddress, port),
// Default to localhost rather than agent/service public IP. The checks Interval: 10 * time.Second,
// can always be overridden if a non-loopback IP is needed. },
TCP: ipaddr.FormatAddressPort(sidecar.Proxy.LocalServiceAddress, sidecar.Port), {
Interval: 10 * time.Second, Name: "Connect Sidecar Aliasing " + serviceID,
}, AliasService: serviceID,
{ },
Name: "Connect Sidecar Aliasing " + ns.ID, }
AliasService: ns.ID,
},
}
}
return sidecar, checks, token, nil
} }

View File

@ -5,6 +5,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/hashicorp/consul/acl"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
@ -16,16 +18,13 @@ func TestAgent_sidecarServiceFromNodeService(t *testing.T) {
} }
tests := []struct { tests := []struct {
name string name string
maxPort int sd *structs.ServiceDefinition
preRegister *structs.ServiceDefinition token string
sd *structs.ServiceDefinition wantNS *structs.NodeService
token string wantChecks []*structs.CheckType
autoPortsDisabled bool wantToken string
wantNS *structs.NodeService wantErr string
wantChecks []*structs.CheckType
wantToken string
wantErr string
}{ }{
{ {
name: "no sidecar", name: "no sidecar",
@ -141,42 +140,6 @@ func TestAgent_sidecarServiceFromNodeService(t *testing.T) {
}, },
wantToken: "custom-token", wantToken: "custom-token",
}, },
{
name: "no auto ports available",
// register another sidecar consuming our 1 and only allocated auto port.
preRegister: &structs.ServiceDefinition{
Kind: structs.ServiceKindConnectProxy,
Name: "api-proxy-sidecar",
Port: 2222, // Consume the one available auto-port
Proxy: &structs.ConnectProxyConfig{
DestinationServiceName: "api",
},
},
sd: &structs.ServiceDefinition{
ID: "web1",
Name: "web",
Port: 1111,
Connect: &structs.ServiceConnect{
SidecarService: &structs.ServiceDefinition{},
},
},
token: "foo",
wantErr: "none left in the configured range [2222, 2222]",
},
{
name: "auto ports disabled",
autoPortsDisabled: true,
sd: &structs.ServiceDefinition{
ID: "web1",
Name: "web",
Port: 1111,
Connect: &structs.ServiceConnect{
SidecarService: &structs.ServiceDefinition{},
},
},
token: "foo",
wantErr: "auto-assignment disabled in config",
},
{ {
name: "inherit tags and meta", name: "inherit tags and meta",
sd: &structs.ServiceDefinition{ sd: &structs.ServiceDefinition{
@ -252,6 +215,58 @@ func TestAgent_sidecarServiceFromNodeService(t *testing.T) {
token: "foo", token: "foo",
wantErr: "reserved for internal use", wantErr: "reserved for internal use",
}, },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hcl := `
ports {
sidecar_min_port = 2222
sidecar_max_port = 2222
}
`
a := StartTestAgent(t, TestAgent{Name: "jones", HCL: hcl})
defer a.Shutdown()
ns := tt.sd.NodeService()
err := ns.Validate()
require.NoError(t, err, "Invalid test case - NodeService must validate")
gotNS, gotChecks, gotToken, err := a.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", name: "re-registering same sidecar with no port should pick same one",
// Allow multiple ports to be sure we get the right one // Allow multiple ports to be sure we get the right one
@ -269,42 +284,27 @@ func TestAgent_sidecarServiceFromNodeService(t *testing.T) {
LocalServicePort: 1111, LocalServicePort: 1111,
}, },
}, },
// Register same again but with different service port // Register same again
sd: &structs.ServiceDefinition{ serviceID: "web1-sidecar-proxy",
ID: "web1", wantPort: 2222, // Should claim the same port as before
Name: "web", },
Port: 1112, {
Connect: &structs.ServiceConnect{ name: "all auto ports already taken",
SidecarService: &structs.ServiceDefinition{}, // register another sidecar consuming our 1 and only allocated auto port.
preRegister: &structs.ServiceDefinition{
Kind: structs.ServiceKindConnectProxy,
Name: "api-proxy-sidecar",
Port: 2222, // Consume the one available auto-port
Proxy: &structs.ConnectProxyConfig{
DestinationServiceName: "api",
}, },
}, },
token: "foo", wantErr: "none left in the configured range [2222, 2222]",
wantNS: &structs.NodeService{ },
EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), {
Kind: structs.ServiceKindConnectProxy, name: "auto ports disabled",
ID: "web1-sidecar-proxy", autoPortsDisabled: true,
Service: "web-sidecar-proxy", wantErr: "auto-assignment disabled in config",
Port: 2222, // Should claim the same port as before
LocallyRegisteredAsSidecar: true,
Proxy: structs.ConnectProxyConfig{
DestinationServiceName: "web",
DestinationServiceID: "web1",
LocalServiceAddress: "127.0.0.1",
LocalServicePort: 1112,
},
},
wantChecks: []*structs.CheckType{
{
Name: "Connect Sidecar Listening",
TCP: "127.0.0.1:2222",
Interval: 10 * time.Second,
},
{
Name: "Connect Sidecar Aliasing web1",
AliasService: "web1",
},
},
wantToken: "foo",
}, },
} }
for _, tt := range tests { for _, tt := range tests {
@ -329,7 +329,6 @@ func TestAgent_sidecarServiceFromNodeService(t *testing.T) {
} }
` `
} }
a := StartTestAgent(t, TestAgent{Name: "jones", HCL: hcl}) a := StartTestAgent(t, TestAgent{Name: "jones", HCL: hcl})
defer a.Shutdown() defer a.Shutdown()
@ -338,11 +337,8 @@ func TestAgent_sidecarServiceFromNodeService(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
} }
ns := tt.sd.NodeService() gotPort, err := a.sidecarPortFromServiceID(structs.ServiceID{ID: tt.serviceID, EnterpriseMeta: tt.enterpriseMeta})
err := ns.ValidateForAgent()
require.NoError(t, err, "Invalid test case - NodeService must validate")
gotNS, gotChecks, gotToken, err := a.sidecarServiceFromNodeService(ns, tt.token)
if tt.wantErr != "" { if tt.wantErr != "" {
require.Error(t, err) require.Error(t, err)
require.Contains(t, err.Error(), tt.wantErr) require.Contains(t, err.Error(), tt.wantErr)
@ -350,9 +346,7 @@ func TestAgent_sidecarServiceFromNodeService(t *testing.T) {
} }
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, tt.wantNS, gotNS) require.Equal(t, tt.wantPort, gotPort)
require.Equal(t, tt.wantChecks, gotChecks)
require.Equal(t, tt.wantToken, gotToken)
}) })
} }
} }