Enable CLI to register terminating gateways (#7500)

* Enable CLI to register terminating gateways

* Centralize gateway proxy configuration
This commit is contained in:
Freddy 2020-03-26 10:20:56 -06:00 committed by GitHub
parent 759ea803e5
commit cb55fa3742
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 465 additions and 94 deletions

View File

@ -210,9 +210,7 @@ func buildAgentService(s *structs.NodeService) api.AgentService {
as.Meta = map[string]string{} as.Meta = map[string]string{}
} }
// Attach Proxy config if exists // Attach Proxy config if exists
if s.Kind == structs.ServiceKindConnectProxy || if s.Kind == structs.ServiceKindConnectProxy || s.IsGateway() {
s.Kind == structs.ServiceKindMeshGateway {
as.Proxy = s.Proxy.ToAPI() as.Proxy = s.Proxy.ToAPI()
} }

View File

@ -201,7 +201,7 @@ func TestAgent_Services_Sidecar(t *testing.T) {
assert.NotContains(string(output), "locally_registered_as_sidecar") assert.NotContains(string(output), "locally_registered_as_sidecar")
} }
// Thie tests that a mesh gateway service is returned as expected. // This tests that a mesh gateway service is returned as expected.
func TestAgent_Services_MeshGateway(t *testing.T) { func TestAgent_Services_MeshGateway(t *testing.T) {
t.Parallel() t.Parallel()
@ -233,6 +233,38 @@ func TestAgent_Services_MeshGateway(t *testing.T) {
require.Equal(t, srv1.Proxy.ToAPI(), actual.Proxy) require.Equal(t, srv1.Proxy.ToAPI(), actual.Proxy)
} }
// This tests that a terminating gateway service is returned as expected.
func TestAgent_Services_TerminatingGateway(t *testing.T) {
t.Parallel()
a := NewTestAgent(t, t.Name(), "")
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
srv1 := &structs.NodeService{
Kind: structs.ServiceKindTerminatingGateway,
ID: "tg-dc1-01",
Service: "tg-dc1",
Port: 8443,
Proxy: structs.ConnectProxyConfig{
Config: map[string]interface{}{
"foo": "bar",
},
},
}
require.NoError(t, a.State.AddService(srv1, ""))
req, _ := http.NewRequest("GET", "/v1/agent/services", nil)
obj, err := a.srv.AgentServices(nil, req)
require.NoError(t, err)
val := obj.(map[string]*api.AgentService)
require.Len(t, val, 1)
actual := val["tg-dc1-01"]
require.NotNil(t, actual)
require.Equal(t, api.ServiceKindTerminatingGateway, actual.Kind)
require.Equal(t, srv1.Proxy.ToAPI(), actual.Proxy)
}
func TestAgent_Services_ACLFilter(t *testing.T) { func TestAgent_Services_ACLFilter(t *testing.T) {
t.Parallel() t.Parallel()
a := NewTestAgent(t, t.Name(), TestACLConfig()) a := NewTestAgent(t, t.Name(), TestACLConfig())

View File

@ -1410,6 +1410,8 @@ func (b *Builder) serviceKindVal(v *string) structs.ServiceKind {
return structs.ServiceKindConnectProxy return structs.ServiceKindConnectProxy
case string(structs.ServiceKindMeshGateway): case string(structs.ServiceKindMeshGateway):
return structs.ServiceKindMeshGateway return structs.ServiceKindMeshGateway
case string(structs.ServiceKindTerminatingGateway):
return structs.ServiceKindTerminatingGateway
default: default:
return structs.ServiceKindTypical return structs.ServiceKindTypical
} }

View File

@ -497,10 +497,23 @@ func registerTestCatalogEntries(t *testing.T, codec rpc.ClientCodec) {
registerTestCatalogEntriesMap(t, codec, registrations) registerTestCatalogEntriesMap(t, codec, registrations)
} }
func registerTestCatalogEntriesMeshGateway(t *testing.T, codec rpc.ClientCodec) { func registerTestCatalogProxyEntries(t *testing.T, codec rpc.ClientCodec) {
t.Helper() t.Helper()
registrations := map[string]*structs.RegisterRequest{ registrations := map[string]*structs.RegisterRequest{
"Service tg-gw": &structs.RegisterRequest{
Datacenter: "dc1",
Node: "terminating-gateway",
ID: types.NodeID("3a9d7530-20d4-443a-98d3-c10fe78f09f4"),
Address: "10.1.2.2",
Service: &structs.NodeService{
Kind: structs.ServiceKindTerminatingGateway,
ID: "tg-gw-01",
Service: "tg-gw",
Port: 8443,
Address: "198.18.1.3",
},
},
"Service mg-gw": &structs.RegisterRequest{ "Service mg-gw": &structs.RegisterRequest{
Datacenter: "dc1", Datacenter: "dc1",
Node: "gateway", Node: "gateway",

View File

@ -611,7 +611,7 @@ func TestInternal_ServiceDump_Kind(t *testing.T) {
// prep the cluster with some data we can use in our filters // prep the cluster with some data we can use in our filters
registerTestCatalogEntries(t, codec) registerTestCatalogEntries(t, codec)
registerTestCatalogEntriesMeshGateway(t, codec) registerTestCatalogProxyEntries(t, codec)
doRequest := func(t *testing.T, kind structs.ServiceKind) structs.CheckServiceNodes { doRequest := func(t *testing.T, kind structs.ServiceKind) structs.CheckServiceNodes {
t.Helper() t.Helper()
@ -633,6 +633,13 @@ func TestInternal_ServiceDump_Kind(t *testing.T) {
require.Len(t, nodes, 9) require.Len(t, nodes, 9)
}) })
t.Run("Terminating Gateway", func(t *testing.T) {
nodes := doRequest(t, structs.ServiceKindTerminatingGateway)
require.Len(t, nodes, 1)
require.Equal(t, "tg-gw", nodes[0].Service.Service)
require.Equal(t, "tg-gw-01", nodes[0].Service.ID)
})
t.Run("Mesh Gateway", func(t *testing.T) { t.Run("Mesh Gateway", func(t *testing.T) {
nodes := doRequest(t, structs.ServiceKindMeshGateway) nodes := doRequest(t, structs.ServiceKindMeshGateway)
require.Len(t, nodes, 1) require.Len(t, nodes, 1)

View File

@ -120,9 +120,9 @@ func (s *ServiceManager) AddService(req *addServiceRequest) error {
req.service.EnterpriseMeta.Normalize() req.service.EnterpriseMeta.Normalize()
// For now only sidecar proxies have anything that can be configured // For now only proxies have anything that can be configured
// centrally. So bypass the whole manager for regular services. // centrally. So bypass the whole manager for regular services.
if !req.service.IsSidecarProxy() && !req.service.IsMeshGateway() { if !req.service.IsSidecarProxy() && !req.service.IsGateway() {
// previousDefaults are ignored here because they are only relevant for central config. // previousDefaults are ignored here because they are only relevant for central config.
req.persistService = nil req.persistService = nil
req.persistDefaults = nil req.persistDefaults = nil

View File

@ -205,6 +205,62 @@ func TestServiceManager_RegisterMeshGateway(t *testing.T) {
}, gateway) }, gateway)
} }
func TestServiceManager_RegisterTerminatingGateway(t *testing.T) {
require := require.New(t)
a := NewTestAgent(t, t.Name(), "enable_central_service_config = true")
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
// Register a global proxy and service config
testApplyConfigEntries(t, a,
&structs.ProxyConfigEntry{
Config: map[string]interface{}{
"foo": 1,
},
},
&structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: "terminating-gateway",
Protocol: "http",
},
)
// Now register a terminating-gateway.
svc := &structs.NodeService{
Kind: structs.ServiceKindTerminatingGateway,
ID: "terminating-gateway",
Service: "terminating-gateway",
Port: 443,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
}
require.NoError(a.AddService(svc, nil, false, "", ConfigSourceLocal))
// Verify gateway got global config loaded
gateway := a.State.Service(structs.NewServiceID("terminating-gateway", nil))
require.NotNil(gateway)
require.Equal(&structs.NodeService{
Kind: structs.ServiceKindTerminatingGateway,
ID: "terminating-gateway",
Service: "terminating-gateway",
Port: 443,
TaggedAddresses: map[string]structs.ServiceAddress{},
Proxy: structs.ConnectProxyConfig{
Config: map[string]interface{}{
"foo": int64(1),
"protocol": "http",
},
},
Weights: &structs.Weights{
Passing: 1,
Warning: 1,
},
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
}, gateway)
}
func TestServiceManager_PersistService_API(t *testing.T) { func TestServiceManager_PersistService_API(t *testing.T) {
// This is the ServiceManager version of TestAgent_PersistService and // This is the ServiceManager version of TestAgent_PersistService and
// TestAgent_PurgeService. // TestAgent_PurgeService.

View File

@ -890,21 +890,11 @@ const (
// service will proxy connections based off the SNI header set by other // service will proxy connections based off the SNI header set by other
// connect proxies // connect proxies
ServiceKindMeshGateway ServiceKind = "mesh-gateway" ServiceKindMeshGateway ServiceKind = "mesh-gateway"
)
func ServiceKindFromString(kind string) (ServiceKind, error) { // ServiceKindTerminatingGateway is a Terminating Gateway for the Connect
switch kind { // feature. This service will proxy connections to services outside the mesh.
case string(ServiceKindTypical): ServiceKindTerminatingGateway ServiceKind = "terminating-gateway"
return ServiceKindTypical, nil )
case string(ServiceKindConnectProxy):
return ServiceKindConnectProxy, nil
case string(ServiceKindMeshGateway):
return ServiceKindMeshGateway, nil
default:
// have to return something and it may as well be typical
return ServiceKindTypical, fmt.Errorf("Invalid service kind: %s", kind)
}
}
// Type to hold a address and port of a service // Type to hold a address and port of a service
type ServiceAddress struct { type ServiceAddress struct {
@ -1048,9 +1038,8 @@ func (s *NodeService) IsSidecarProxy() bool {
return s.Kind == ServiceKindConnectProxy && s.Proxy.DestinationServiceID != "" return s.Kind == ServiceKindConnectProxy && s.Proxy.DestinationServiceID != ""
} }
func (s *NodeService) IsMeshGateway() bool { func (s *NodeService) IsGateway() bool {
// TODO (mesh-gateway) any other things to check? return s.Kind == ServiceKindMeshGateway || s.Kind == ServiceKindTerminatingGateway
return s.Kind == ServiceKindMeshGateway
} }
// Validate validates the node service configuration. // Validate validates the node service configuration.
@ -1147,36 +1136,36 @@ func (s *NodeService) Validate() error {
} }
} }
// MeshGateway validation // Gateway validation
if s.Kind == ServiceKindMeshGateway { if s.IsGateway() {
// Gateways must have a port // Gateways must have a port
if s.Port == 0 { if s.Port == 0 {
result = multierror.Append(result, fmt.Errorf("Port must be non-zero for a Mesh Gateway")) result = multierror.Append(result, fmt.Errorf("Port must be non-zero for a %s", s.Kind))
} }
// Gateways cannot have sidecars // Gateways cannot have sidecars
if s.Connect.SidecarService != nil { if s.Connect.SidecarService != nil {
result = multierror.Append(result, fmt.Errorf("Mesh Gateways cannot have a sidecar service defined")) result = multierror.Append(result, fmt.Errorf("A %s cannot have a sidecar service defined", s.Kind))
} }
if s.Proxy.DestinationServiceName != "" { if s.Proxy.DestinationServiceName != "" {
result = multierror.Append(result, fmt.Errorf("The Proxy.DestinationServiceName configuration is invalid for Mesh Gateways")) result = multierror.Append(result, fmt.Errorf("The Proxy.DestinationServiceName configuration is invalid for a %s", s.Kind))
} }
if s.Proxy.DestinationServiceID != "" { if s.Proxy.DestinationServiceID != "" {
result = multierror.Append(result, fmt.Errorf("The Proxy.DestinationServiceID configuration is invalid for Mesh Gateways")) result = multierror.Append(result, fmt.Errorf("The Proxy.DestinationServiceID configuration is invalid for a %s", s.Kind))
} }
if s.Proxy.LocalServiceAddress != "" { if s.Proxy.LocalServiceAddress != "" {
result = multierror.Append(result, fmt.Errorf("The Proxy.LocalServiceAddress configuration is invalid for Mesh Gateways")) result = multierror.Append(result, fmt.Errorf("The Proxy.LocalServiceAddress configuration is invalid for a %s", s.Kind))
} }
if s.Proxy.LocalServicePort != 0 { if s.Proxy.LocalServicePort != 0 {
result = multierror.Append(result, fmt.Errorf("The Proxy.LocalServicePort configuration is invalid for Mesh Gateways")) result = multierror.Append(result, fmt.Errorf("The Proxy.LocalServicePort configuration is invalid for a %s", s.Kind))
} }
if len(s.Proxy.Upstreams) != 0 { if len(s.Proxy.Upstreams) != 0 {
result = multierror.Append(result, fmt.Errorf("The Proxy.Upstreams configuration is invalid for Mesh Gateways")) result = multierror.Append(result, fmt.Errorf("The Proxy.Upstreams configuration is invalid for a %s", s.Kind))
} }
} }

View File

@ -415,6 +415,58 @@ func TestStructs_NodeService_ValidateMeshGateway(t *testing.T) {
} }
} }
func TestStructs_NodeService_ValidateTerminatingGateway(t *testing.T) {
type testCase struct {
Modify func(*NodeService)
Err string
}
cases := map[string]testCase{
"valid": testCase{
func(x *NodeService) {},
"",
},
"sidecar-service": testCase{
func(x *NodeService) { x.Connect.SidecarService = &ServiceDefinition{} },
"cannot have a sidecar service",
},
"proxy-destination-name": testCase{
func(x *NodeService) { x.Proxy.DestinationServiceName = "foo" },
"Proxy.DestinationServiceName configuration is invalid",
},
"proxy-destination-id": testCase{
func(x *NodeService) { x.Proxy.DestinationServiceID = "foo" },
"Proxy.DestinationServiceID configuration is invalid",
},
"proxy-local-address": testCase{
func(x *NodeService) { x.Proxy.LocalServiceAddress = "127.0.0.1" },
"Proxy.LocalServiceAddress configuration is invalid",
},
"proxy-local-port": testCase{
func(x *NodeService) { x.Proxy.LocalServicePort = 36 },
"Proxy.LocalServicePort configuration is invalid",
},
"proxy-upstreams": testCase{
func(x *NodeService) { x.Proxy.Upstreams = []Upstream{Upstream{}} },
"Proxy.Upstreams configuration is invalid",
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
ns := TestNodeServiceTerminatingGateway(t, "10.0.0.5")
tc.Modify(ns)
err := ns.Validate()
if tc.Err == "" {
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, strings.ToLower(err.Error()), strings.ToLower(tc.Err))
}
})
}
}
func TestStructs_NodeService_ValidateExposeConfig(t *testing.T) { func TestStructs_NodeService_ValidateExposeConfig(t *testing.T) {
type testCase struct { type testCase struct {
Modify func(*NodeService) Modify func(*NodeService)

View File

@ -85,6 +85,15 @@ func TestNodeServiceMeshGateway(t testing.T) *NodeService {
ServiceAddress{Address: "198.18.4.5", Port: 443}) ServiceAddress{Address: "198.18.4.5", Port: 443})
} }
func TestNodeServiceTerminatingGateway(t testing.T, address string) *NodeService {
return &NodeService{
Kind: ServiceKindTerminatingGateway,
Port: 8443,
Service: "terminating-gateway",
Address: address,
}
}
func TestNodeServiceMeshGatewayWithAddrs(t testing.T, address string, port int, lanAddr, wanAddr ServiceAddress) *NodeService { func TestNodeServiceMeshGatewayWithAddrs(t testing.T, address string, port int, lanAddr, wanAddr ServiceAddress) *NodeService {
return &NodeService{ return &NodeService{
Kind: ServiceKindMeshGateway, Kind: ServiceKindMeshGateway,

View File

@ -480,7 +480,7 @@ func (s *Server) makeMeshGatewayCluster(clusterName string, cfgSnap *proxycfg.Co
// defaults to use the mesh gateway timeout. // defaults to use the mesh gateway timeout.
func (s *Server) makeMeshGatewayClusterWithConnectTimeout(clusterName string, cfgSnap *proxycfg.ConfigSnapshot, func (s *Server) makeMeshGatewayClusterWithConnectTimeout(clusterName string, cfgSnap *proxycfg.ConfigSnapshot,
connectTimeout time.Duration) (*envoy.Cluster, error) { connectTimeout time.Duration) (*envoy.Cluster, error) {
cfg, err := ParseMeshGatewayConfig(cfgSnap.Proxy.Config) cfg, err := ParseGatewayConfig(cfgSnap.Proxy.Config)
if err != nil { if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns // Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue. // default config if there is an error so it's safe to continue.

View File

@ -1,6 +1,7 @@
package xds package xds
import ( import (
"github.com/hashicorp/consul/lib"
"strings" "strings"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
@ -67,7 +68,7 @@ func ParseProxyConfig(m map[string]interface{}) (ProxyConfig, error) {
return cfg, err return cfg, err
} }
type MeshGatewayConfig struct { type GatewayConfig struct {
// BindTaggedAddresses when set will cause all of the services tagged // BindTaggedAddresses when set will cause all of the services tagged
// addresses to have listeners bound to them in addition to the main service // addresses to have listeners bound to them in addition to the main service
// address listener. This is only suitable when the tagged addresses are IP // address listener. This is only suitable when the tagged addresses are IP
@ -75,27 +76,32 @@ type MeshGatewayConfig struct {
// for those addresses or where an external entity maps that IP to the Envoy // for those addresses or where an external entity maps that IP to the Envoy
// (like AWS EC2 mapping a public IP to the private interface) then this // (like AWS EC2 mapping a public IP to the private interface) then this
// cannot be used. See the BindAddresses config instead // cannot be used. See the BindAddresses config instead
// BindTaggedAddresses bool `mapstructure:"envoy_gateway_bind_tagged_addresses"`
// TODO - wow this is a verbose setting name. Maybe shorten this
BindTaggedAddresses bool `mapstructure:"envoy_mesh_gateway_bind_tagged_addresses"`
// BindAddresses additional bind addresses to configure listeners for // BindAddresses additional bind addresses to configure listeners for
BindAddresses map[string]structs.ServiceAddress `mapstructure:"envoy_mesh_gateway_bind_addresses"` BindAddresses map[string]structs.ServiceAddress `mapstructure:"envoy_gateway_bind_addresses"`
// NoDefaultBind indicates that we should not bind to the default address of the // NoDefaultBind indicates that we should not bind to the default address of the
// gateway service // gateway service
NoDefaultBind bool `mapstructure:"envoy_mesh_gateway_no_default_bind"` NoDefaultBind bool `mapstructure:"envoy_gateway_no_default_bind"`
// ConnectTimeoutMs is the number of milliseconds to timeout making a new // ConnectTimeoutMs is the number of milliseconds to timeout making a new
// connection to this upstream. Defaults to 5000 (5 seconds) if not set. // connection to this upstream. Defaults to 5000 (5 seconds) if not set.
ConnectTimeoutMs int `mapstructure:"connect_timeout_ms"` ConnectTimeoutMs int `mapstructure:"connect_timeout_ms"`
} }
// ParseMeshGatewayConfig returns the MeshGatewayConfig parsed from an opaque map. If an // ParseGatewayConfig returns the GatewayConfig parsed from an opaque map. If an
// error occurs during parsing, it is returned along with the default config. This // error occurs during parsing, it is returned along with the default config. This
// allows the caller to choose whether and how to report the error // allows the caller to choose whether and how to report the error
func ParseMeshGatewayConfig(m map[string]interface{}) (MeshGatewayConfig, error) { func ParseGatewayConfig(m map[string]interface{}) (GatewayConfig, error) {
var cfg MeshGatewayConfig // Fixup for deprecated mesh gateway names
lib.TranslateKeys(m, map[string]string{
"envoy_mesh_gateway_bind_tagged_addresses": "envoy_gateway_bind_tagged_addresses",
"envoy_mesh_gateway_bind_addresses": "envoy_gateway_bind_addresses",
"envoy_mesh_gateway_no_default_bind": "envoy_gateway_no_default_bind",
})
var cfg GatewayConfig
err := mapstructure.WeakDecode(m, &cfg) err := mapstructure.WeakDecode(m, &cfg)
if cfg.ConnectTimeoutMs < 1 { if cfg.ConnectTimeoutMs < 1 {

View File

@ -1,6 +1,7 @@
package xds package xds
import ( import (
"github.com/hashicorp/consul/agent/structs"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -253,6 +254,96 @@ func TestParseUpstreamConfig(t *testing.T) {
} }
} }
func TestParseGatewayConfig(t *testing.T) {
tests := []struct {
name string
input map[string]interface{}
want GatewayConfig
}{
{
name: "defaults - nil",
input: nil,
want: GatewayConfig{
ConnectTimeoutMs: 5000,
},
},
{
name: "defaults - empty",
input: map[string]interface{}{},
want: GatewayConfig{
ConnectTimeoutMs: 5000,
},
},
{
name: "defaults - other stuff",
input: map[string]interface{}{
"foo": "bar",
"envoy_foo": "envoy_bar",
},
want: GatewayConfig{
ConnectTimeoutMs: 5000,
},
},
{
name: "kitchen sink",
input: map[string]interface{}{
"envoy_gateway_bind_tagged_addresses": true,
"envoy_gateway_bind_addresses": map[string]structs.ServiceAddress{"foo": {Address: "127.0.0.1", Port: 80}},
"envoy_gateway_no_default_bind": true,
"connect_timeout_ms": 10,
},
want: GatewayConfig{
ConnectTimeoutMs: 10,
BindTaggedAddresses: true,
NoDefaultBind: true,
BindAddresses: map[string]structs.ServiceAddress{"foo": {Address: "127.0.0.1", Port: 80}},
},
},
{
name: "deprecated kitchen sink",
input: map[string]interface{}{
"envoy_mesh_gateway_bind_tagged_addresses": true,
"envoy_mesh_gateway_bind_addresses": map[string]structs.ServiceAddress{"foo": {Address: "127.0.0.1", Port: 80}},
"envoy_mesh_gateway_no_default_bind": true,
"connect_timeout_ms": 10,
},
want: GatewayConfig{
ConnectTimeoutMs: 10,
BindTaggedAddresses: true,
NoDefaultBind: true,
BindAddresses: map[string]structs.ServiceAddress{"foo": {Address: "127.0.0.1", Port: 80}},
},
},
{
name: "new fields override deprecated ones",
input: map[string]interface{}{
// Deprecated
"envoy_mesh_gateway_bind_tagged_addresses": true,
"envoy_mesh_gateway_bind_addresses": map[string]structs.ServiceAddress{"foo": {Address: "127.0.0.1", Port: 80}},
"envoy_mesh_gateway_no_default_bind": true,
// New
"envoy_gateway_bind_tagged_addresses": false,
"envoy_gateway_bind_addresses": map[string]structs.ServiceAddress{"bar": {Address: "127.0.0.1", Port: 8080}},
"envoy_gateway_no_default_bind": false,
},
want: GatewayConfig{
ConnectTimeoutMs: 5000,
BindTaggedAddresses: false,
NoDefaultBind: false,
BindAddresses: map[string]structs.ServiceAddress{"bar": {Address: "127.0.0.1", Port: 8080}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseGatewayConfig(tt.input)
require.NoError(t, err)
require.Equal(t, tt.want, got)
})
}
}
func intPointer(i int) *int { func intPointer(i int) *int {
return &i return &i
} }

View File

@ -181,7 +181,7 @@ func parseCheckPath(check structs.CheckType) (structs.ExposePath, error) {
// listenersFromSnapshotMeshGateway returns the "listener" for a mesh-gateway service // listenersFromSnapshotMeshGateway returns the "listener" for a mesh-gateway service
func (s *Server) listenersFromSnapshotMeshGateway(cfgSnap *proxycfg.ConfigSnapshot, token string) ([]proto.Message, error) { func (s *Server) listenersFromSnapshotMeshGateway(cfgSnap *proxycfg.ConfigSnapshot, token string) ([]proto.Message, error) {
cfg, err := ParseMeshGatewayConfig(cfgSnap.Proxy.Config) cfg, err := ParseGatewayConfig(cfgSnap.Proxy.Config)
if err != nil { if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns // Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue. // default config if there is an error so it's safe to continue.

View File

@ -28,6 +28,10 @@ const (
// service will proxy connections based off the SNI header set by other // service will proxy connections based off the SNI header set by other
// connect proxies // connect proxies
ServiceKindMeshGateway ServiceKind = "mesh-gateway" ServiceKindMeshGateway ServiceKind = "mesh-gateway"
// ServiceKindTerminatingGateway is a Terminating Gateway for the Connect
// feature. This service will proxy connections to services outside the mesh.
ServiceKindTerminatingGateway ServiceKind = "terminating-gateway"
) )
// UpstreamDestType is the type of upstream discovery mechanism. // UpstreamDestType is the type of upstream discovery mechanism.

View File

@ -1705,6 +1705,37 @@ func TestAgentService_Register_MeshGateway(t *testing.T) {
require.Equal(t, "bar", svc.Proxy.Config["foo"]) require.Equal(t, "bar", svc.Proxy.Config["foo"])
} }
func TestAgentService_Register_TerminatingGateway(t *testing.T) {
t.Parallel()
c, s := makeClient(t)
defer s.Stop()
agent := c.Agent()
reg := AgentServiceRegistration{
Kind: ServiceKindTerminatingGateway,
Name: "terminating-gateway",
Address: "10.1.2.3",
Port: 8443,
Proxy: &AgentServiceConnectProxyConfig{
Config: map[string]interface{}{
"foo": "bar",
},
},
}
err := agent.ServiceRegister(&reg)
require.NoError(t, err)
svc, _, err := agent.Service("terminating-gateway", nil)
require.NoError(t, err)
require.NotNil(t, svc)
require.Equal(t, ServiceKindTerminatingGateway, svc.Kind)
require.NotNil(t, svc.Proxy)
require.Contains(t, svc.Proxy.Config, "foo")
require.Equal(t, "bar", svc.Proxy.Config["foo"])
}
func TestAgentService_ExposeChecks(t *testing.T) { func TestAgentService_ExposeChecks(t *testing.T) {
t.Parallel() t.Parallel()
c, s := makeClient(t) c, s := makeClient(t)

View File

@ -46,6 +46,7 @@ type cmd struct {
// flags // flags
meshGateway bool meshGateway bool
gateway string
proxyID string proxyID string
sidecarFor string sidecarFor string
adminAccessLogPath string adminAccessLogPath string
@ -64,10 +65,19 @@ type cmd struct {
bindAddresses ServiceAddressMapValue bindAddresses ServiceAddressMapValue
exposeServers bool exposeServers bool
meshGatewaySvcName string gatewaySvcName string
gatewayKind api.ServiceKind
} }
const defaultEnvoyVersion = "1.13.1" const (
defaultEnvoyVersion = "1.13.1"
meshGatewayVal = "mesh"
)
var supportedGateways = map[string]api.ServiceKind{
"mesh": api.ServiceKindMeshGateway,
"terminating": api.ServiceKindTerminatingGateway,
}
func (c *cmd) init() { func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError) c.flags = flag.NewFlagSet("", flag.ContinueOnError)
@ -75,9 +85,13 @@ func (c *cmd) init() {
c.flags.StringVar(&c.proxyID, "proxy-id", os.Getenv("CONNECT_PROXY_ID"), c.flags.StringVar(&c.proxyID, "proxy-id", os.Getenv("CONNECT_PROXY_ID"),
"The proxy's ID on the local agent.") "The proxy's ID on the local agent.")
// Deprecated in favor of `gateway`
c.flags.BoolVar(&c.meshGateway, "mesh-gateway", false, c.flags.BoolVar(&c.meshGateway, "mesh-gateway", false,
"Configure Envoy as a Mesh Gateway.") "Configure Envoy as a Mesh Gateway.")
c.flags.StringVar(&c.gateway, "gateway", "",
"The type of gateway to register. One of: terminating or mesh")
c.flags.StringVar(&c.sidecarFor, "sidecar-for", os.Getenv("CONNECT_SIDECAR_FOR"), c.flags.StringVar(&c.sidecarFor, "sidecar-for", os.Getenv("CONNECT_SIDECAR_FOR"),
"The ID of a service instance on the local agent that this proxy should "+ "The ID of a service instance on the local agent that this proxy should "+
"become a sidecar for. It requires that the proxy service is registered "+ "become a sidecar for. It requires that the proxy service is registered "+
@ -128,7 +142,7 @@ func (c *cmd) init() {
"address to use instead of the default binding rules given as `<name>=<ip>:<port>` "+ "address to use instead of the default binding rules given as `<name>=<ip>:<port>` "+
"pairs. This flag may be specified multiple times to add multiple bind addresses.") "pairs. This flag may be specified multiple times to add multiple bind addresses.")
c.flags.StringVar(&c.meshGatewaySvcName, "service", "mesh-gateway", c.flags.StringVar(&c.gatewaySvcName, "service", "",
"Service name to use for the registration") "Service name to use for the registration")
c.flags.BoolVar(&c.exposeServers, "expose-servers", false, c.flags.BoolVar(&c.exposeServers, "expose-servers", false,
@ -195,8 +209,17 @@ func (c *cmd) Run(args []string) int {
} }
c.client = client c.client = client
// Fixup for deprecated mesh-gateway flag
if c.meshGateway && c.gateway != "" {
c.UI.Error("The mesh-gateway flag is deprecated and cannot be used alongside the gateway flag")
return 1
}
if c.meshGateway {
c.gateway = meshGatewayVal
}
if c.exposeServers { if c.exposeServers {
if !c.meshGateway { if c.gateway != meshGatewayVal {
c.UI.Error("'-expose-servers' can only be used for mesh gateways") c.UI.Error("'-expose-servers' can only be used for mesh gateways")
return 1 return 1
} }
@ -207,11 +230,22 @@ func (c *cmd) Run(args []string) int {
} }
if c.register { if c.register {
if !c.meshGateway { if c.gateway == "" {
c.UI.Error("Auto-Registration can only be used for mesh gateways") c.UI.Error("Auto-Registration can only be used for gateways")
return 1 return 1
} }
kind, ok := supportedGateways[c.gateway]
if !ok {
c.UI.Error("Gateway must be one of: terminating or mesh")
return 1
}
c.gatewayKind = kind
if c.gatewaySvcName == "" {
c.gatewaySvcName = string(c.gatewayKind)
}
taggedAddrs := make(map[string]api.ServiceAddress) taggedAddrs := make(map[string]api.ServiceAddress)
lanAddr := c.lanAddress.Value() lanAddr := c.lanAddress.Value()
if lanAddr.Address != "" { if lanAddr.Address != "" {
@ -231,13 +265,12 @@ func (c *cmd) Run(args []string) int {
} }
var proxyConf *api.AgentServiceConnectProxyConfig var proxyConf *api.AgentServiceConnectProxyConfig
if len(c.bindAddresses.value) > 0 { if len(c.bindAddresses.value) > 0 {
// override all default binding rules and just bind to the user-supplied addresses // override all default binding rules and just bind to the user-supplied addresses
proxyConf = &api.AgentServiceConnectProxyConfig{ proxyConf = &api.AgentServiceConnectProxyConfig{
Config: map[string]interface{}{ Config: map[string]interface{}{
"envoy_mesh_gateway_no_default_bind": true, "envoy_gateway_no_default_bind": true,
"envoy_mesh_gateway_bind_addresses": c.bindAddresses.value, "envoy_gateway_bind_addresses": c.bindAddresses.value,
}, },
} }
} else if canBind(lanAddr) && canBind(wanAddr) { } else if canBind(lanAddr) && canBind(wanAddr) {
@ -245,8 +278,8 @@ func (c *cmd) Run(args []string) int {
// for creating the envoy listeners // for creating the envoy listeners
proxyConf = &api.AgentServiceConnectProxyConfig{ proxyConf = &api.AgentServiceConnectProxyConfig{
Config: map[string]interface{}{ Config: map[string]interface{}{
"envoy_mesh_gateway_no_default_bind": true, "envoy_gateway_no_default_bind": true,
"envoy_mesh_gateway_bind_tagged_addresses": true, "envoy_gateway_bind_tagged_addresses": true,
}, },
} }
} else if !canBind(lanAddr) && lanAddr.Address != "" { } else if !canBind(lanAddr) && lanAddr.Address != "" {
@ -260,15 +293,15 @@ func (c *cmd) Run(args []string) int {
} }
svc := api.AgentServiceRegistration{ svc := api.AgentServiceRegistration{
Kind: api.ServiceKindMeshGateway, Kind: c.gatewayKind,
Name: c.meshGatewaySvcName, Name: c.gatewaySvcName,
Address: lanAddr.Address, Address: lanAddr.Address,
Port: lanAddr.Port, Port: lanAddr.Port,
Meta: meta, Meta: meta,
TaggedAddresses: taggedAddrs, TaggedAddresses: taggedAddrs,
Proxy: proxyConf, Proxy: proxyConf,
Check: &api.AgentServiceCheck{ Check: &api.AgentServiceCheck{
Name: "Mesh Gateway Listening", Name: fmt.Sprintf("%s listening", c.gatewayKind),
TCP: ipaddr.FormatAddressPort(tcpCheckAddr, lanAddr.Port), TCP: ipaddr.FormatAddressPort(tcpCheckAddr, lanAddr.Port),
Interval: "10s", Interval: "10s",
DeregisterCriticalServiceAfter: c.deregAfterCritical, DeregisterCriticalServiceAfter: c.deregAfterCritical,
@ -291,18 +324,19 @@ func (c *cmd) Run(args []string) int {
return 1 return 1
} }
c.proxyID = proxyID c.proxyID = proxyID
} else if c.proxyID == "" && c.meshGateway { } else if c.proxyID == "" && c.gateway != "" {
gatewaySvc, err := proxyCmd.LookupGatewayProxy(c.client) gatewaySvc, err := proxyCmd.LookupGatewayProxy(c.client, c.gatewayKind)
if err != nil { if err != nil {
c.UI.Error(err.Error()) c.UI.Error(err.Error())
return 1 return 1
} }
c.proxyID = gatewaySvc.ID c.proxyID = gatewaySvc.ID
c.meshGatewaySvcName = gatewaySvc.Service c.gatewaySvcName = gatewaySvc.Service
} }
if c.proxyID == "" { if c.proxyID == "" {
c.UI.Error("No proxy ID specified. One of -proxy-id or -sidecar-for/-mesh-gateway is required") c.UI.Error("No proxy ID specified. One of -proxy-id or -sidecar-for/-gateway is " +
"required")
return 1 return 1
} }
@ -443,8 +477,8 @@ func (c *cmd) templateArgs() (*BootstrapTplArgs, error) {
cluster := c.proxyID cluster := c.proxyID
if c.sidecarFor != "" { if c.sidecarFor != "" {
cluster = c.sidecarFor cluster = c.sidecarFor
} else if c.meshGateway && c.meshGatewaySvcName != "" { } else if c.gateway != "" && c.gatewaySvcName != "" {
cluster = c.meshGatewaySvcName cluster = c.gatewaySvcName
} }
adminAccessLogPath := c.adminAccessLogPath adminAccessLogPath := c.adminAccessLogPath

View File

@ -3,6 +3,12 @@ package envoy
import ( import (
"encoding/json" "encoding/json"
"flag" "flag"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/agent/xds"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
"io/ioutil" "io/ioutil"
"net" "net"
"net/http" "net/http"
@ -11,13 +17,6 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/agent/xds"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
) )
var update = flag.Bool("update", false, "update golden files") var update = flag.Bool("update", false, "update golden files")
@ -29,6 +28,50 @@ func TestEnvoyCommand_noTabs(t *testing.T) {
} }
} }
func TestEnvoyGateway_Validation(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
output string
}{
{
"-register for non-gateway",
[]string{"-register"},
"Auto-Registration can only be used for gateways",
},
{
"-mesh-gateway and -gateway cannot be combined",
[]string{"-register", "-mesh-gateway", "-gateway", "mesh"},
"The mesh-gateway flag is deprecated and cannot be used alongside the gateway flag",
},
{
"no proxy registration specified nor discovered",
[]string{""},
"No proxy ID specified",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
ui := cli.NewMockUi()
c := New(ui)
c.init()
code := c.Run(tc.args)
if code == 0 {
t.Errorf("%s: expected non-zero exit", tc.name)
}
output := ui.ErrorWriter.String()
if !strings.Contains(output, tc.output) {
t.Errorf("expected %q to contain %q", output, tc.output)
}
})
}
}
// testSetAndResetEnv sets the env vars passed as KEY=value strings in the // testSetAndResetEnv sets the env vars passed as KEY=value strings in the
// current ENV and returns a func() that will undo it's work at the end of the // current ENV and returns a func() that will undo it's work at the end of the
// test for use with defer. // test for use with defer.

View File

@ -11,7 +11,7 @@ import (
"github.com/hashicorp/go-sockaddr/template" "github.com/hashicorp/go-sockaddr/template"
) )
const defaultMeshGatewayPort int = 443 const defaultGatewayPort int = 443
// ServiceAddressValue implements a flag.Value that may be used to parse an // ServiceAddressValue implements a flag.Value that may be used to parse an
// addr:port string into an api.ServiceAddress. // addr:port string into an api.ServiceAddress.
@ -21,14 +21,14 @@ type ServiceAddressValue struct {
func (s *ServiceAddressValue) String() string { func (s *ServiceAddressValue) String() string {
if s == nil { if s == nil {
return fmt.Sprintf(":%d", defaultMeshGatewayPort) return fmt.Sprintf(":%d", defaultGatewayPort)
} }
return fmt.Sprintf("%v:%d", s.value.Address, s.value.Port) return fmt.Sprintf("%v:%d", s.value.Address, s.value.Port)
} }
func (s *ServiceAddressValue) Value() api.ServiceAddress { func (s *ServiceAddressValue) Value() api.ServiceAddress {
if s == nil || s.value.Port == 0 && s.value.Address == "" { if s == nil || s.value.Port == 0 && s.value.Address == "" {
return api.ServiceAddress{Port: defaultMeshGatewayPort} return api.ServiceAddress{Port: defaultGatewayPort}
} }
return s.value return s.value
} }
@ -51,7 +51,7 @@ func parseAddress(raw string) (api.ServiceAddress, error) {
return result, fmt.Errorf("Error parsing address %q: %v", x, err) return result, fmt.Errorf("Error parsing address %q: %v", x, err)
} }
port := defaultMeshGatewayPort port := defaultGatewayPort
if portStr != "" { if portStr != "" {
port, err = strconv.Atoi(portStr) port, err = strconv.Atoi(portStr)
if err != nil { if err != nil {

View File

@ -10,12 +10,12 @@ import (
func TestServiceAddressValue_Value(t *testing.T) { func TestServiceAddressValue_Value(t *testing.T) {
t.Run("nil receiver", func(t *testing.T) { t.Run("nil receiver", func(t *testing.T) {
var addr *ServiceAddressValue var addr *ServiceAddressValue
require.Equal(t, addr.Value(), api.ServiceAddress{Port: defaultMeshGatewayPort}) require.Equal(t, addr.Value(), api.ServiceAddress{Port: defaultGatewayPort})
}) })
t.Run("default value", func(t *testing.T) { t.Run("default value", func(t *testing.T) {
addr := &ServiceAddressValue{} addr := &ServiceAddressValue{}
require.Equal(t, addr.Value(), api.ServiceAddress{Port: defaultMeshGatewayPort}) require.Equal(t, addr.Value(), api.ServiceAddress{Port: defaultGatewayPort})
}) })
t.Run("set value", func(t *testing.T) { t.Run("set value", func(t *testing.T) {
@ -40,7 +40,7 @@ func TestServiceAddressValue_Set(t *testing.T) {
input: "8.8.8.8:", input: "8.8.8.8:",
expectedValue: api.ServiceAddress{ expectedValue: api.ServiceAddress{
Address: "8.8.8.8", Address: "8.8.8.8",
Port: defaultMeshGatewayPort, Port: defaultGatewayPort,
}, },
}, },
{ {

View File

@ -248,13 +248,13 @@ func LookupProxyIDForSidecar(client *api.Client, sidecarFor string) (string, err
return proxyIDs[0], nil return proxyIDs[0], nil
} }
// LookupGatewayProxyID finds the mesh-gateway service registered with the local // LookupGatewayProxyID finds the gateway service registered with the local
// agent if any and returns its service ID. It will return an ID if and only if // agent if any and returns its service ID. It will return an ID if and only if
// there is exactly one registered mesh-gateway registered to the agent. // there is exactly one gateway of this kind registered to the agent.
func LookupGatewayProxy(client *api.Client) (*api.AgentService, error) { func LookupGatewayProxy(client *api.Client, kind api.ServiceKind) (*api.AgentService, error) {
svcs, err := client.Agent().ServicesWithFilter("Kind == `mesh-gateway`") svcs, err := client.Agent().ServicesWithFilter(fmt.Sprintf("Kind == `%s`", kind))
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed looking up mesh-gateway instances: %v", err) return nil, fmt.Errorf("Failed looking up %s instances: %v", kind, err)
} }
var proxyIDs []string var proxyIDs []string
@ -264,14 +264,14 @@ func LookupGatewayProxy(client *api.Client) (*api.AgentService, error) {
switch len(svcs) { switch len(svcs) {
case 0: case 0:
return nil, fmt.Errorf("No mesh-gateway services registered with this agent") return nil, fmt.Errorf("No %s services registered with this agent", kind)
case 1: case 1:
for _, svc := range svcs { for _, svc := range svcs {
return svc, nil return svc, nil
} }
return nil, fmt.Errorf("This should be unreachable") return nil, fmt.Errorf("This should be unreachable")
default: default:
return nil, fmt.Errorf("Cannot lookup the mesh-gateway's proxy ID because multiple are registered with the agent") return nil, fmt.Errorf("Cannot lookup the %s's proxy ID because multiple are registered with the agent", kind)
} }
} }

View File

@ -279,7 +279,7 @@ definition](/docs/connect/registration/service-registration.html) or
since HTTP/2 has many requests per connection. For this configuration to be since HTTP/2 has many requests per connection. For this configuration to be
respected, a L7 protocol must be defined in the `protocol` field. respected, a L7 protocol must be defined in the `protocol` field.
### Mesh Gateway Options ### Gateway Options
These fields may also be overridden explicitly in the [proxy service These fields may also be overridden explicitly in the [proxy service
definition](/docs/connect/registration/service-registration.html), or defined in definition](/docs/connect/registration/service-registration.html), or defined in
@ -287,25 +287,29 @@ the [global `proxy-defaults` configuration
entry](/docs/agent/config_entries.html#proxy-defaults-proxy-defaults) to act as entry](/docs/agent/config_entries.html#proxy-defaults-proxy-defaults) to act as
defaults that are inherited by all services. defaults that are inherited by all services.
Prior to 1.8.0 these settings were specific to Mesh Gateways. The deprecated
names such as `envoy_mesh_gateway_bind_addresses` and `envoy_mesh_gateway_no_default_bind`
will continue to be supported.
- `connect_timeout_ms` - The number of milliseconds to allow when making upstream - `connect_timeout_ms` - The number of milliseconds to allow when making upstream
connections before timing out. Defaults to 5000 (5 seconds). If the upstream connections before timing out. Defaults to 5000 (5 seconds). If the upstream
service has the configuration option service has the configuration option
[`connect_timeout_ms`](/docs/agent/config-entries/service-resolver.html#connecttimeout) [`connect_timeout_ms`](/docs/agent/config-entries/service-resolver.html#connecttimeout)
set for the `service-resolver`, that timeout value will take precedence over set for the `service-resolver`, that timeout value will take precedence over
this mesh gateway option. this gateway option.
- `envoy_mesh_gateway_bind_tagged_addresses` - Indicates that the mesh gateway - `envoy_gateway_bind_tagged_addresses` - Indicates that the gateway
services tagged addresses should be bound to listeners in addition to the services tagged addresses should be bound to listeners in addition to the
default listener address. default listener address.
- `envoy_mesh_gateway_bind_addresses` - A map of additional addresses to be bound. - `envoy_gateway_bind_addresses` - A map of additional addresses to be bound.
This map's keys are the name of the listeners to be created and the values are This map's keys are the name of the listeners to be created and the values are
a map with two keys, address and port, that combined make the address to bind the a map with two keys, address and port, that combined make the address to bind the
listener to. These are bound in addition to the default address. listener to. These are bound in addition to the default address.
- `envoy_mesh_gateway_no_default_bind` - Prevents binding to the default address - `envoy_gateway_no_default_bind` - Prevents binding to the default address
of the mesh gateway service. This should be used with one of the other options of the gateway service. This should be used with one of the other options
to configure the gateways bind addresses. to configure the gateway's bind addresses.
## Advanced Configuration ## Advanced Configuration