Ingress Gateways for TCP services (#7509)
* Implements a simple, tcp ingress gateway workflow This adds a new type of gateway for allowing Ingress traffic into Connect from external services. Co-authored-by: Chris Piraino <cpiraino@hashicorp.com>
This commit is contained in:
parent
12b026db62
commit
6a5eba63ab
|
@ -4294,6 +4294,15 @@ func (a *Agent) registerCache() {
|
||||||
RefreshTimeout: 10 * time.Minute,
|
RefreshTimeout: 10 * time.Minute,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
a.cache.RegisterType(cachetype.GatewayServicesName, &cachetype.GatewayServices{
|
||||||
|
RPC: a,
|
||||||
|
}, &cache.RegisterOptions{
|
||||||
|
// Maintain a blocking query, retry dropped connections quickly
|
||||||
|
Refresh: true,
|
||||||
|
RefreshTimer: 0 * time.Second,
|
||||||
|
RefreshTimeout: 10 * time.Minute,
|
||||||
|
})
|
||||||
|
|
||||||
a.cache.RegisterType(cachetype.ConfigEntriesName, &cachetype.ConfigEntries{
|
a.cache.RegisterType(cachetype.ConfigEntriesName, &cachetype.ConfigEntries{
|
||||||
RPC: a,
|
RPC: a,
|
||||||
}, &cache.RegisterOptions{
|
}, &cache.RegisterOptions{
|
||||||
|
|
|
@ -19,7 +19,7 @@ type CatalogServices struct {
|
||||||
func (c *CatalogServices) Fetch(opts cache.FetchOptions, req cache.Request) (cache.FetchResult, error) {
|
func (c *CatalogServices) Fetch(opts cache.FetchOptions, req cache.Request) (cache.FetchResult, error) {
|
||||||
var result cache.FetchResult
|
var result cache.FetchResult
|
||||||
|
|
||||||
// The request should be a DCSpecificRequest.
|
// The request should be a ServiceSpecificRequest.
|
||||||
reqReal, ok := req.(*structs.ServiceSpecificRequest)
|
reqReal, ok := req.(*structs.ServiceSpecificRequest)
|
||||||
if !ok {
|
if !ok {
|
||||||
return result, fmt.Errorf(
|
return result, fmt.Errorf(
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
package cachetype
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/agent/cache"
|
||||||
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Recommended name for registration.
|
||||||
|
const GatewayServicesName = "gateway-services"
|
||||||
|
|
||||||
|
// GatewayUpstreams supports fetching upstreams for a given gateway name.
|
||||||
|
type GatewayServices struct {
|
||||||
|
RPC RPC
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GatewayServices) Fetch(opts cache.FetchOptions, req cache.Request) (cache.FetchResult, error) {
|
||||||
|
var result cache.FetchResult
|
||||||
|
|
||||||
|
// The request should be a ServiceSpecificRequest.
|
||||||
|
reqReal, ok := req.(*structs.ServiceSpecificRequest)
|
||||||
|
if !ok {
|
||||||
|
return result, fmt.Errorf(
|
||||||
|
"Internal cache failure: request wrong type: %T", req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lightweight copy this object so that manipulating QueryOptions doesn't race.
|
||||||
|
dup := *reqReal
|
||||||
|
reqReal = &dup
|
||||||
|
|
||||||
|
// Set the minimum query index to our current index so we block
|
||||||
|
reqReal.QueryOptions.MinQueryIndex = opts.MinIndex
|
||||||
|
reqReal.QueryOptions.MaxQueryTime = opts.Timeout
|
||||||
|
|
||||||
|
// Always allow stale - there's no point in hitting leader if the request is
|
||||||
|
// going to be served from cache and end up arbitrarily stale anyway. This
|
||||||
|
// allows cached service-discover to automatically read scale across all
|
||||||
|
// servers too.
|
||||||
|
reqReal.AllowStale = true
|
||||||
|
|
||||||
|
// Fetch
|
||||||
|
var reply structs.IndexedGatewayServices
|
||||||
|
if err := g.RPC.RPC("Internal.GatewayServices", reqReal, &reply); err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Value = &reply
|
||||||
|
result.Index = reply.QueryMeta.Index
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GatewayServices) SupportsBlocking() bool {
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package cachetype
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/agent/cache"
|
||||||
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGatewayServices(t *testing.T) {
|
||||||
|
rpc := TestRPC(t)
|
||||||
|
typ := &GatewayServices{RPC: rpc}
|
||||||
|
|
||||||
|
// Expect the proper RPC call. This also sets the expected value
|
||||||
|
// since that is return-by-pointer in the arguments.
|
||||||
|
var resp *structs.IndexedGatewayServices
|
||||||
|
rpc.On("RPC", "Internal.GatewayServices", mock.Anything, mock.Anything).Return(nil).
|
||||||
|
Run(func(args mock.Arguments) {
|
||||||
|
req := args.Get(1).(*structs.ServiceSpecificRequest)
|
||||||
|
require.Equal(t, uint64(24), req.QueryOptions.MinQueryIndex)
|
||||||
|
require.Equal(t, 1*time.Second, req.QueryOptions.MaxQueryTime)
|
||||||
|
require.True(t, req.AllowStale)
|
||||||
|
require.Equal(t, "foo", req.ServiceName)
|
||||||
|
|
||||||
|
services := structs.GatewayServices{
|
||||||
|
{
|
||||||
|
Service: structs.NewServiceID("api", nil),
|
||||||
|
Gateway: structs.NewServiceID("gateway", nil),
|
||||||
|
GatewayKind: structs.ServiceKindIngressGateway,
|
||||||
|
Port: 1234,
|
||||||
|
CAFile: "api/ca.crt",
|
||||||
|
CertFile: "api/client.crt",
|
||||||
|
KeyFile: "api/client.key",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
reply := args.Get(2).(*structs.IndexedGatewayServices)
|
||||||
|
reply.Services = services
|
||||||
|
reply.QueryMeta.Index = 48
|
||||||
|
resp = reply
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fetch
|
||||||
|
resultA, err := typ.Fetch(cache.FetchOptions{
|
||||||
|
MinIndex: 24,
|
||||||
|
Timeout: 1 * time.Second,
|
||||||
|
}, &structs.ServiceSpecificRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
ServiceName: "foo",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, cache.FetchResult{
|
||||||
|
Value: resp,
|
||||||
|
Index: 48,
|
||||||
|
}, resultA)
|
||||||
|
|
||||||
|
rpc.AssertExpectations(t)
|
||||||
|
}
|
|
@ -1417,6 +1417,8 @@ func (b *Builder) serviceKindVal(v *string) structs.ServiceKind {
|
||||||
return structs.ServiceKindMeshGateway
|
return structs.ServiceKindMeshGateway
|
||||||
case string(structs.ServiceKindTerminatingGateway):
|
case string(structs.ServiceKindTerminatingGateway):
|
||||||
return structs.ServiceKindTerminatingGateway
|
return structs.ServiceKindTerminatingGateway
|
||||||
|
case string(structs.ServiceKindIngressGateway):
|
||||||
|
return structs.ServiceKindIngressGateway
|
||||||
default:
|
default:
|
||||||
return structs.ServiceKindTypical
|
return structs.ServiceKindTypical
|
||||||
}
|
}
|
||||||
|
|
|
@ -213,24 +213,6 @@ func TestConfig_Apply_TerminatingGateway(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, 200, resp.Code, "!200 Response Code: %s", resp.Body.String())
|
require.Equal(t, 200, resp.Code, "!200 Response Code: %s", resp.Body.String())
|
||||||
|
|
||||||
// Attempt to create an entry for a separate gateway that also routes to web
|
|
||||||
body = bytes.NewBuffer([]byte(`
|
|
||||||
{
|
|
||||||
"Kind": "terminating-gateway",
|
|
||||||
"Name": "east-gw-01",
|
|
||||||
"Services": [
|
|
||||||
{
|
|
||||||
"Name": "web",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`))
|
|
||||||
|
|
||||||
req, _ = http.NewRequest("PUT", "/v1/config", body)
|
|
||||||
resp = httptest.NewRecorder()
|
|
||||||
_, err = a.srv.ConfigApply(resp, req)
|
|
||||||
require.Error(t, err, "service \"web\" is associated with a different gateway")
|
|
||||||
require.Equal(t, 200, resp.Code, "!200 Response Code: %s", resp.Body.String())
|
|
||||||
|
|
||||||
// List all entries, there should only be one
|
// List all entries, there should only be one
|
||||||
{
|
{
|
||||||
args := structs.ConfigEntryQuery{
|
args := structs.ConfigEntryQuery{
|
||||||
|
@ -258,6 +240,67 @@ func TestConfig_Apply_TerminatingGateway(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfig_Apply_IngressGateway(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
a := NewTestAgent(t, "")
|
||||||
|
defer a.Shutdown()
|
||||||
|
testrpc.WaitForTestAgent(t, a.RPC, "dc1")
|
||||||
|
|
||||||
|
// Create some config entries.
|
||||||
|
body := bytes.NewBuffer([]byte(`
|
||||||
|
{
|
||||||
|
"Kind": "ingress-gateway",
|
||||||
|
"Name": "ingress",
|
||||||
|
"Listeners": [
|
||||||
|
{
|
||||||
|
"Port": 8080,
|
||||||
|
"Services": [
|
||||||
|
{ "Name": "web" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`))
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("PUT", "/v1/config", body)
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
_, err := a.srv.ConfigApply(resp, req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 200, resp.Code, "!200 Response Code: %s", resp.Body.String())
|
||||||
|
|
||||||
|
// List all entries, there should only be one
|
||||||
|
{
|
||||||
|
args := structs.ConfigEntryQuery{
|
||||||
|
Kind: structs.IngressGateway,
|
||||||
|
Datacenter: "dc1",
|
||||||
|
}
|
||||||
|
var out structs.IndexedConfigEntries
|
||||||
|
require.NoError(t, a.RPC("ConfigEntry.List", &args, &out))
|
||||||
|
require.NotNil(t, out)
|
||||||
|
require.Len(t, out.Entries, 1)
|
||||||
|
|
||||||
|
got := out.Entries[0].(*structs.IngressGatewayConfigEntry)
|
||||||
|
// Ignore create and modify indices
|
||||||
|
got.CreateIndex = 0
|
||||||
|
got.ModifyIndex = 0
|
||||||
|
|
||||||
|
expect := &structs.IngressGatewayConfigEntry{
|
||||||
|
Name: "ingress",
|
||||||
|
Kind: structs.IngressGateway,
|
||||||
|
Listeners: []structs.IngressListener{
|
||||||
|
{
|
||||||
|
Port: 8080,
|
||||||
|
Protocol: "tcp",
|
||||||
|
Services: []structs.IngressService{
|
||||||
|
{Name: "web"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.Equal(t, expect, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestConfig_Apply_ProxyDefaultsMeshGateway(t *testing.T) {
|
func TestConfig_Apply_ProxyDefaultsMeshGateway(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|
|
@ -187,6 +187,8 @@ func (h *Health) ServiceNodes(args *structs.ServiceSpecificRequest, reply *struc
|
||||||
f = h.serviceNodesConnect
|
f = h.serviceNodesConnect
|
||||||
case args.TagFilter:
|
case args.TagFilter:
|
||||||
f = h.serviceNodesTagFilter
|
f = h.serviceNodesTagFilter
|
||||||
|
case args.Ingress:
|
||||||
|
f = h.serviceNodesIngress
|
||||||
default:
|
default:
|
||||||
f = h.serviceNodesDefault
|
f = h.serviceNodesDefault
|
||||||
}
|
}
|
||||||
|
@ -201,9 +203,9 @@ func (h *Health) ServiceNodes(args *structs.ServiceSpecificRequest, reply *struc
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're doing a connect query, we need read access to the service
|
// If we're doing a connect or ingress query, we need read access to the service
|
||||||
// we're trying to find proxies for, so check that.
|
// we're trying to find proxies for, so check that.
|
||||||
if args.Connect {
|
if args.Connect || args.Ingress {
|
||||||
if authz != nil && authz.ServiceRead(args.ServiceName, &authzContext) != acl.Allow {
|
if authz != nil && authz.ServiceRead(args.ServiceName, &authzContext) != acl.Allow {
|
||||||
// Just return nil, which will return an empty response (tested)
|
// Just return nil, which will return an empty response (tested)
|
||||||
return nil
|
return nil
|
||||||
|
@ -249,6 +251,9 @@ func (h *Health) ServiceNodes(args *structs.ServiceSpecificRequest, reply *struc
|
||||||
if args.Connect {
|
if args.Connect {
|
||||||
key = "connect"
|
key = "connect"
|
||||||
}
|
}
|
||||||
|
if args.Ingress {
|
||||||
|
key = "ingress"
|
||||||
|
}
|
||||||
|
|
||||||
metrics.IncrCounterWithLabels([]string{"health", key, "query"}, 1,
|
metrics.IncrCounterWithLabels([]string{"health", key, "query"}, 1,
|
||||||
[]metrics.Label{{Name: "service", Value: args.ServiceName}})
|
[]metrics.Label{{Name: "service", Value: args.ServiceName}})
|
||||||
|
@ -284,6 +289,10 @@ func (h *Health) serviceNodesConnect(ws memdb.WatchSet, s *state.Store, args *st
|
||||||
return s.CheckConnectServiceNodes(ws, args.ServiceName, &args.EnterpriseMeta)
|
return s.CheckConnectServiceNodes(ws, args.ServiceName, &args.EnterpriseMeta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Health) serviceNodesIngress(ws memdb.WatchSet, s *state.Store, args *structs.ServiceSpecificRequest) (uint64, structs.CheckServiceNodes, error) {
|
||||||
|
return s.CheckIngressServiceNodes(ws, args.ServiceName, &args.EnterpriseMeta)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Health) serviceNodesTagFilter(ws memdb.WatchSet, s *state.Store, args *structs.ServiceSpecificRequest) (uint64, structs.CheckServiceNodes, error) {
|
func (h *Health) serviceNodesTagFilter(ws memdb.WatchSet, s *state.Store, args *structs.ServiceSpecificRequest) (uint64, structs.CheckServiceNodes, error) {
|
||||||
// DEPRECATED (singular-service-tag) - remove this when backwards RPC compat
|
// DEPRECATED (singular-service-tag) - remove this when backwards RPC compat
|
||||||
// with 1.2.x is not required.
|
// with 1.2.x is not required.
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||||
"github.com/hashicorp/consul/testrpc"
|
"github.com/hashicorp/consul/testrpc"
|
||||||
"github.com/hashicorp/consul/types"
|
"github.com/hashicorp/consul/types"
|
||||||
"github.com/hashicorp/net-rpc-msgpackrpc"
|
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -1142,6 +1142,214 @@ func TestHealth_ServiceNodes_Gateway(t *testing.T) {
|
||||||
assert.Equal(r, 443, resp.Nodes[1].Service.Port)
|
assert.Equal(r, 443, resp.Nodes[1].Service.Port)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
func TestHealth_ServiceNodes_Ingress(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
dir1, s1 := testServer(t)
|
||||||
|
defer os.RemoveAll(dir1)
|
||||||
|
defer s1.Shutdown()
|
||||||
|
codec := rpcClient(t, s1)
|
||||||
|
defer codec.Close()
|
||||||
|
|
||||||
|
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||||
|
|
||||||
|
arg := structs.RegisterRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foo",
|
||||||
|
Address: "127.0.0.1",
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
ID: "ingress-gateway",
|
||||||
|
Service: "ingress-gateway",
|
||||||
|
Kind: structs.ServiceKindIngressGateway,
|
||||||
|
},
|
||||||
|
Check: &structs.HealthCheck{
|
||||||
|
Name: "ingress connect",
|
||||||
|
Status: api.HealthPassing,
|
||||||
|
ServiceID: "ingress-gateway",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var out struct{}
|
||||||
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Catalog.Register", &arg, &out))
|
||||||
|
|
||||||
|
arg = structs.RegisterRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "bar",
|
||||||
|
Address: "127.0.0.2",
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
ID: "ingress-gateway",
|
||||||
|
Service: "ingress-gateway",
|
||||||
|
Kind: structs.ServiceKindIngressGateway,
|
||||||
|
},
|
||||||
|
Check: &structs.HealthCheck{
|
||||||
|
Name: "ingress connect",
|
||||||
|
Status: api.HealthWarning,
|
||||||
|
ServiceID: "ingress-gateway",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Catalog.Register", &arg, &out))
|
||||||
|
|
||||||
|
// Register ingress-gateway config entry
|
||||||
|
{
|
||||||
|
args := &structs.IngressGatewayConfigEntry{
|
||||||
|
Name: "ingress-gateway",
|
||||||
|
Kind: structs.IngressGateway,
|
||||||
|
Listeners: []structs.IngressListener{
|
||||||
|
{
|
||||||
|
Port: 8888,
|
||||||
|
Services: []structs.IngressService{
|
||||||
|
{Name: "db"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := structs.ConfigEntryRequest{
|
||||||
|
Op: structs.ConfigEntryUpsert,
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Entry: args,
|
||||||
|
}
|
||||||
|
var out bool
|
||||||
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &req, &out))
|
||||||
|
require.True(t, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
var out2 structs.IndexedCheckServiceNodes
|
||||||
|
req := structs.ServiceSpecificRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
ServiceName: "db",
|
||||||
|
Ingress: true,
|
||||||
|
}
|
||||||
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Health.ServiceNodes", &req, &out2))
|
||||||
|
|
||||||
|
nodes := out2.Nodes
|
||||||
|
require.Len(t, nodes, 2)
|
||||||
|
require.Equal(t, nodes[0].Node.Node, "bar")
|
||||||
|
require.Equal(t, nodes[0].Checks[0].Status, api.HealthWarning)
|
||||||
|
require.Equal(t, nodes[1].Node.Node, "foo")
|
||||||
|
require.Equal(t, nodes[1].Checks[0].Status, api.HealthPassing)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHealth_ServiceNodes_Ingress_ACL(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||||
|
c.ACLDatacenter = "dc1"
|
||||||
|
c.ACLsEnabled = true
|
||||||
|
c.ACLMasterToken = "root"
|
||||||
|
c.ACLDefaultPolicy = "deny"
|
||||||
|
c.ACLEnforceVersion8 = true
|
||||||
|
})
|
||||||
|
defer os.RemoveAll(dir1)
|
||||||
|
defer s1.Shutdown()
|
||||||
|
codec := rpcClient(t, s1)
|
||||||
|
defer codec.Close()
|
||||||
|
|
||||||
|
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||||
|
|
||||||
|
// Create the ACL.
|
||||||
|
token, err := upsertTestTokenWithPolicyRules(codec, "root", "dc1", `
|
||||||
|
service "db" { policy = "read" }
|
||||||
|
service "ingress-gateway" { policy = "read" }
|
||||||
|
node_prefix "" { policy = "read" }`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
arg := structs.RegisterRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foo",
|
||||||
|
Address: "127.0.0.1",
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
ID: "ingress-gateway",
|
||||||
|
Service: "ingress-gateway",
|
||||||
|
},
|
||||||
|
Check: &structs.HealthCheck{
|
||||||
|
Name: "ingress connect",
|
||||||
|
Status: api.HealthPassing,
|
||||||
|
ServiceID: "ingress-gateway",
|
||||||
|
},
|
||||||
|
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||||
|
}
|
||||||
|
var out struct{}
|
||||||
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Catalog.Register", &arg, &out))
|
||||||
|
|
||||||
|
arg = structs.RegisterRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "bar",
|
||||||
|
Address: "127.0.0.2",
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
ID: "ingress-gateway",
|
||||||
|
Service: "ingress-gateway",
|
||||||
|
},
|
||||||
|
Check: &structs.HealthCheck{
|
||||||
|
Name: "ingress connect",
|
||||||
|
Status: api.HealthWarning,
|
||||||
|
ServiceID: "ingress-gateway",
|
||||||
|
},
|
||||||
|
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||||
|
}
|
||||||
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Catalog.Register", &arg, &out))
|
||||||
|
|
||||||
|
// Register ingress-gateway config entry
|
||||||
|
{
|
||||||
|
args := &structs.IngressGatewayConfigEntry{
|
||||||
|
Name: "ingress-gateway",
|
||||||
|
Kind: structs.IngressGateway,
|
||||||
|
Listeners: []structs.IngressListener{
|
||||||
|
{
|
||||||
|
Port: 8888,
|
||||||
|
Protocol: "http",
|
||||||
|
Services: []structs.IngressService{
|
||||||
|
{Name: "db"},
|
||||||
|
{Name: "another"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := structs.ConfigEntryRequest{
|
||||||
|
Op: structs.ConfigEntryUpsert,
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Entry: args,
|
||||||
|
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||||
|
}
|
||||||
|
var out bool
|
||||||
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &req, &out))
|
||||||
|
require.True(t, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// No token used
|
||||||
|
var out2 structs.IndexedCheckServiceNodes
|
||||||
|
req := structs.ServiceSpecificRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
ServiceName: "db",
|
||||||
|
Ingress: true,
|
||||||
|
}
|
||||||
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Health.ServiceNodes", &req, &out2))
|
||||||
|
require.Len(t, out2.Nodes, 0)
|
||||||
|
|
||||||
|
// Requesting a service that is not covered by the token's policy
|
||||||
|
req = structs.ServiceSpecificRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
ServiceName: "another",
|
||||||
|
Ingress: true,
|
||||||
|
QueryOptions: structs.QueryOptions{Token: token.SecretID},
|
||||||
|
}
|
||||||
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Health.ServiceNodes", &req, &out2))
|
||||||
|
require.Len(t, out2.Nodes, 0)
|
||||||
|
|
||||||
|
// Requesting service covered by the token's policy
|
||||||
|
req = structs.ServiceSpecificRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
ServiceName: "db",
|
||||||
|
Ingress: true,
|
||||||
|
QueryOptions: structs.QueryOptions{Token: token.SecretID},
|
||||||
|
}
|
||||||
|
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Health.ServiceNodes", &req, &out2))
|
||||||
|
|
||||||
|
nodes := out2.Nodes
|
||||||
|
require.Len(t, nodes, 2)
|
||||||
|
require.Equal(t, nodes[0].Node.Node, "bar")
|
||||||
|
require.Equal(t, nodes[0].Checks[0].Status, api.HealthWarning)
|
||||||
|
require.Equal(t, nodes[1].Node.Node, "foo")
|
||||||
|
require.Equal(t, nodes[1].Checks[0].Status, api.HealthPassing)
|
||||||
|
}
|
||||||
|
|
||||||
func TestHealth_NodeChecks_FilterACL(t *testing.T) {
|
func TestHealth_NodeChecks_FilterACL(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
|
@ -323,12 +323,26 @@ func (m *Internal) GatewayServices(args *structs.ServiceSpecificRequest, reply *
|
||||||
var index uint64
|
var index uint64
|
||||||
var services structs.GatewayServices
|
var services structs.GatewayServices
|
||||||
|
|
||||||
switch args.ServiceKind {
|
supportedGateways := []string{structs.IngressGateway, structs.TerminatingGateway}
|
||||||
case structs.ServiceKindTerminatingGateway:
|
var found bool
|
||||||
index, services, err = state.TerminatingGatewayServices(ws, args.ServiceName, &args.EnterpriseMeta)
|
for _, kind := range supportedGateways {
|
||||||
|
// We only use this call to validate the RPC call, don't add the watch set
|
||||||
|
_, entry, err := state.ConfigEntry(nil, kind, args.ServiceName, &args.EnterpriseMeta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if entry != nil {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("service %q is not a configured terminating-gateway or ingress-gateway", args.ServiceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
index, services, err = state.GatewayServices(ws, args.ServiceName, &args.EnterpriseMeta)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.srv.filterACL(args.Token, &services); err != nil {
|
if err := m.srv.filterACL(args.Token, &services); err != nil {
|
||||||
|
|
|
@ -2,11 +2,12 @@ package consul
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/acl"
|
"github.com/hashicorp/consul/acl"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
"github.com/hashicorp/consul/api"
|
"github.com/hashicorp/consul/api"
|
||||||
|
@ -752,7 +753,6 @@ func TestInternal_TerminatingGatewayServices(t *testing.T) {
|
||||||
req := structs.ServiceSpecificRequest{
|
req := structs.ServiceSpecificRequest{
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
ServiceName: "gateway",
|
ServiceName: "gateway",
|
||||||
ServiceKind: structs.ServiceKindTerminatingGateway,
|
|
||||||
}
|
}
|
||||||
var resp structs.IndexedGatewayServices
|
var resp structs.IndexedGatewayServices
|
||||||
assert.Nil(r, msgpackrpc.CallWithCodec(codec, "Internal.GatewayServices", &req, &resp))
|
assert.Nil(r, msgpackrpc.CallWithCodec(codec, "Internal.GatewayServices", &req, &resp))
|
||||||
|
@ -784,11 +784,175 @@ func TestInternal_TerminatingGatewayServices(t *testing.T) {
|
||||||
KeyFile: "client.key",
|
KeyFile: "client.key",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore raft index for equality
|
||||||
|
for _, s := range resp.Services {
|
||||||
|
s.RaftIndex = structs.RaftIndex{}
|
||||||
|
}
|
||||||
assert.Equal(r, expect, resp.Services)
|
assert.Equal(r, expect, resp.Services)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInternal_TerminatingGatewayServices_ACLFiltering(t *testing.T) {
|
func TestInternal_GatewayServices_BothGateways(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
dir1, s1 := testServer(t)
|
||||||
|
defer os.RemoveAll(dir1)
|
||||||
|
defer s1.Shutdown()
|
||||||
|
|
||||||
|
codec := rpcClient(t, s1)
|
||||||
|
defer codec.Close()
|
||||||
|
|
||||||
|
testrpc.WaitForTestAgent(t, s1.RPC, "dc1")
|
||||||
|
{
|
||||||
|
var out struct{}
|
||||||
|
|
||||||
|
// Register a service "api"
|
||||||
|
args := structs.TestRegisterRequest(t)
|
||||||
|
args.Service.Service = "api"
|
||||||
|
args.Check = &structs.HealthCheck{
|
||||||
|
Name: "api",
|
||||||
|
Status: api.HealthPassing,
|
||||||
|
ServiceID: args.Service.Service,
|
||||||
|
}
|
||||||
|
assert.Nil(t, msgpackrpc.CallWithCodec(codec, "Catalog.Register", &args, &out))
|
||||||
|
|
||||||
|
// Register a terminating gateway
|
||||||
|
args = &structs.RegisterRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foo",
|
||||||
|
Address: "127.0.0.1",
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
Kind: structs.ServiceKindTerminatingGateway,
|
||||||
|
Service: "gateway",
|
||||||
|
Port: 443,
|
||||||
|
},
|
||||||
|
Check: &structs.HealthCheck{
|
||||||
|
Name: "gateway",
|
||||||
|
Status: api.HealthPassing,
|
||||||
|
ServiceID: "gateway",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Nil(t, msgpackrpc.CallWithCodec(codec, "Catalog.Register", &args, &out))
|
||||||
|
|
||||||
|
entryArgs := &structs.ConfigEntryRequest{
|
||||||
|
Op: structs.ConfigEntryUpsert,
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Entry: &structs.TerminatingGatewayConfigEntry{
|
||||||
|
Kind: "terminating-gateway",
|
||||||
|
Name: "gateway",
|
||||||
|
Services: []structs.LinkedService{
|
||||||
|
{
|
||||||
|
Name: "api",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var entryResp bool
|
||||||
|
assert.Nil(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &entryArgs, &entryResp))
|
||||||
|
|
||||||
|
// Register a service "db"
|
||||||
|
args = structs.TestRegisterRequest(t)
|
||||||
|
args.Service.Service = "db"
|
||||||
|
args.Check = &structs.HealthCheck{
|
||||||
|
Name: "db",
|
||||||
|
Status: api.HealthPassing,
|
||||||
|
ServiceID: args.Service.Service,
|
||||||
|
}
|
||||||
|
assert.Nil(t, msgpackrpc.CallWithCodec(codec, "Catalog.Register", &args, &out))
|
||||||
|
|
||||||
|
// Register an ingress gateway
|
||||||
|
args = &structs.RegisterRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foo",
|
||||||
|
Address: "127.0.0.2",
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
Kind: structs.ServiceKindTerminatingGateway,
|
||||||
|
Service: "ingress",
|
||||||
|
Port: 444,
|
||||||
|
},
|
||||||
|
Check: &structs.HealthCheck{
|
||||||
|
Name: "ingress",
|
||||||
|
Status: api.HealthPassing,
|
||||||
|
ServiceID: "ingress",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Nil(t, msgpackrpc.CallWithCodec(codec, "Catalog.Register", &args, &out))
|
||||||
|
|
||||||
|
entryArgs = &structs.ConfigEntryRequest{
|
||||||
|
Op: structs.ConfigEntryUpsert,
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Entry: &structs.IngressGatewayConfigEntry{
|
||||||
|
Kind: "ingress-gateway",
|
||||||
|
Name: "ingress",
|
||||||
|
Listeners: []structs.IngressListener{
|
||||||
|
{
|
||||||
|
Port: 8888,
|
||||||
|
Services: []structs.IngressService{
|
||||||
|
{Name: "db"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Nil(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &entryArgs, &entryResp))
|
||||||
|
}
|
||||||
|
|
||||||
|
retry.Run(t, func(r *retry.R) {
|
||||||
|
req := structs.ServiceSpecificRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
ServiceName: "gateway",
|
||||||
|
}
|
||||||
|
var resp structs.IndexedGatewayServices
|
||||||
|
assert.Nil(r, msgpackrpc.CallWithCodec(codec, "Internal.GatewayServices", &req, &resp))
|
||||||
|
assert.Len(r, resp.Services, 1)
|
||||||
|
|
||||||
|
expect := structs.GatewayServices{
|
||||||
|
{
|
||||||
|
Service: structs.NewServiceID("api", nil),
|
||||||
|
Gateway: structs.NewServiceID("gateway", nil),
|
||||||
|
GatewayKind: structs.ServiceKindTerminatingGateway,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore raft index for equality
|
||||||
|
for _, s := range resp.Services {
|
||||||
|
s.RaftIndex = structs.RaftIndex{}
|
||||||
|
}
|
||||||
|
assert.Equal(r, expect, resp.Services)
|
||||||
|
|
||||||
|
req.ServiceName = "ingress"
|
||||||
|
assert.Nil(r, msgpackrpc.CallWithCodec(codec, "Internal.GatewayServices", &req, &resp))
|
||||||
|
assert.Len(r, resp.Services, 1)
|
||||||
|
|
||||||
|
expect = structs.GatewayServices{
|
||||||
|
{
|
||||||
|
Service: structs.NewServiceID("db", nil),
|
||||||
|
Gateway: structs.NewServiceID("ingress", nil),
|
||||||
|
GatewayKind: structs.ServiceKindIngressGateway,
|
||||||
|
Port: 8888,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore raft index for equality
|
||||||
|
for _, s := range resp.Services {
|
||||||
|
s.RaftIndex = structs.RaftIndex{}
|
||||||
|
}
|
||||||
|
assert.Equal(r, expect, resp.Services)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test a non-gateway service being requested
|
||||||
|
req := structs.ServiceSpecificRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
ServiceName: "api",
|
||||||
|
}
|
||||||
|
var resp structs.IndexedGatewayServices
|
||||||
|
err := msgpackrpc.CallWithCodec(codec, "Internal.GatewayServices", &req, &resp)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), `service "api" is not a configured terminating-gateway or ingress-gateway`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternal_GatewayServices_ACLFiltering(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||||
|
@ -907,7 +1071,6 @@ service_prefix "db" {
|
||||||
req := structs.ServiceSpecificRequest{
|
req := structs.ServiceSpecificRequest{
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
ServiceName: "gateway",
|
ServiceName: "gateway",
|
||||||
ServiceKind: structs.ServiceKindTerminatingGateway,
|
|
||||||
QueryOptions: structs.QueryOptions{Token: svcToken.SecretID},
|
QueryOptions: structs.QueryOptions{Token: svcToken.SecretID},
|
||||||
}
|
}
|
||||||
var resp structs.IndexedGatewayServices
|
var resp structs.IndexedGatewayServices
|
||||||
|
@ -928,7 +1091,6 @@ service "gateway" {
|
||||||
req := structs.ServiceSpecificRequest{
|
req := structs.ServiceSpecificRequest{
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
ServiceName: "gateway",
|
ServiceName: "gateway",
|
||||||
ServiceKind: structs.ServiceKindTerminatingGateway,
|
|
||||||
QueryOptions: structs.QueryOptions{Token: gwToken.SecretID},
|
QueryOptions: structs.QueryOptions{Token: gwToken.SecretID},
|
||||||
}
|
}
|
||||||
var resp structs.IndexedGatewayServices
|
var resp structs.IndexedGatewayServices
|
||||||
|
@ -952,7 +1114,6 @@ service "gateway" {
|
||||||
req := structs.ServiceSpecificRequest{
|
req := structs.ServiceSpecificRequest{
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
ServiceName: "gateway",
|
ServiceName: "gateway",
|
||||||
ServiceKind: structs.ServiceKindTerminatingGateway,
|
|
||||||
QueryOptions: structs.QueryOptions{Token: validToken.SecretID},
|
QueryOptions: structs.QueryOptions{Token: validToken.SecretID},
|
||||||
}
|
}
|
||||||
var resp structs.IndexedGatewayServices
|
var resp structs.IndexedGatewayServices
|
||||||
|
@ -971,6 +1132,11 @@ service "gateway" {
|
||||||
GatewayKind: structs.ServiceKindTerminatingGateway,
|
GatewayKind: structs.ServiceKindTerminatingGateway,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore raft index for equality
|
||||||
|
for _, s := range resp.Services {
|
||||||
|
s.RaftIndex = structs.RaftIndex{}
|
||||||
|
}
|
||||||
assert.Equal(r, expect, resp.Services)
|
assert.Equal(r, expect, resp.Services)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
servicesTableName = "services"
|
servicesTableName = "services"
|
||||||
terminatingGatewayServicesTableName = "terminating-gateway-services"
|
gatewayServicesTableName = "gateway-services"
|
||||||
|
|
||||||
// serviceLastExtinctionIndexName keeps track of the last raft index when the last instance
|
// serviceLastExtinctionIndexName keeps track of the last raft index when the last instance
|
||||||
// of any service was unregistered. This is used by blocking queries on missing services.
|
// of any service was unregistered. This is used by blocking queries on missing services.
|
||||||
|
@ -57,11 +57,11 @@ func nodesTableSchema() *memdb.TableSchema {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// terminatingGatewayServicesTableSchema returns a new table schema used to store information
|
// gatewayServicesTableNameSchema returns a new table schema used to store information
|
||||||
// about services associated with terminating gateways.
|
// about services associated with terminating gateways.
|
||||||
func terminatingGatewayServicesTableSchema() *memdb.TableSchema {
|
func gatewayServicesTableNameSchema() *memdb.TableSchema {
|
||||||
return &memdb.TableSchema{
|
return &memdb.TableSchema{
|
||||||
Name: terminatingGatewayServicesTableName,
|
Name: gatewayServicesTableName,
|
||||||
Indexes: map[string]*memdb.IndexSchema{
|
Indexes: map[string]*memdb.IndexSchema{
|
||||||
"id": {
|
"id": {
|
||||||
Name: "id",
|
Name: "id",
|
||||||
|
@ -158,7 +158,7 @@ func init() {
|
||||||
registerSchema(nodesTableSchema)
|
registerSchema(nodesTableSchema)
|
||||||
registerSchema(servicesTableSchema)
|
registerSchema(servicesTableSchema)
|
||||||
registerSchema(checksTableSchema)
|
registerSchema(checksTableSchema)
|
||||||
registerSchema(terminatingGatewayServicesTableSchema)
|
registerSchema(gatewayServicesTableNameSchema)
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -775,14 +775,21 @@ func (s *Store) ensureServiceTxn(tx *memdb.Txn, idx uint64, node string, svc *st
|
||||||
return fmt.Errorf("Invalid Service Meta for node %s and serviceID %s: %v", node, svc.ID, err)
|
return fmt.Errorf("Invalid Service Meta for node %s and serviceID %s: %v", node, svc.ID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this service is covered by a terminating gateway's wildcard specifier
|
// Check if this service is covered by a gateway's wildcard specifier
|
||||||
gateway, err := s.serviceTerminatingGateway(tx, structs.WildcardSpecifier, &svc.EnterpriseMeta)
|
svcGateways, err := s.serviceGateways(tx, structs.WildcardSpecifier, &svc.EnterpriseMeta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed gateway lookup for %q: %s", svc.Service, err)
|
return fmt.Errorf("failed gateway lookup for %q: %s", svc.Service, err)
|
||||||
}
|
}
|
||||||
if gatewaySvc, ok := gateway.(*structs.GatewayService); ok && gatewaySvc != nil {
|
for service := svcGateways.Next(); service != nil; service = svcGateways.Next() {
|
||||||
if err = s.updateTerminatingGatewayService(tx, idx, gatewaySvc.Gateway, svc.Service, &svc.EnterpriseMeta); err != nil {
|
if wildcardSvc, ok := service.(*structs.GatewayService); ok && wildcardSvc != nil {
|
||||||
return fmt.Errorf("Failed to associate service %q with gateway %q", gatewaySvc.Service.String(), gatewaySvc.Gateway.String())
|
|
||||||
|
// Copy the wildcard mapping and modify it
|
||||||
|
gatewaySvc := wildcardSvc.Clone()
|
||||||
|
gatewaySvc.Service = structs.NewServiceID(svc.Service, &svc.EnterpriseMeta)
|
||||||
|
|
||||||
|
if err = s.updateGatewayService(tx, idx, gatewaySvc); err != nil {
|
||||||
|
return fmt.Errorf("Failed to associate service %q with gateway %q", gatewaySvc.Service.String(), gatewaySvc.Gateway.String())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -863,6 +870,10 @@ func (s *Store) ServiceList(ws memdb.WatchSet, entMeta *structs.EnterpriseMeta)
|
||||||
tx := s.db.Txn(false)
|
tx := s.db.Txn(false)
|
||||||
defer tx.Abort()
|
defer tx.Abort()
|
||||||
|
|
||||||
|
return s.serviceListTxn(tx, ws, entMeta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) serviceListTxn(tx *memdb.Txn, ws memdb.WatchSet, entMeta *structs.EnterpriseMeta) (uint64, structs.ServiceList, error) {
|
||||||
idx := s.catalogServicesMaxIndex(tx, entMeta)
|
idx := s.catalogServicesMaxIndex(tx, entMeta)
|
||||||
|
|
||||||
services, err := s.catalogServiceList(tx, entMeta, true)
|
services, err := s.catalogServiceList(tx, entMeta, true)
|
||||||
|
@ -1040,11 +1051,14 @@ func (s *Store) serviceNodes(ws memdb.WatchSet, serviceName string, connect bool
|
||||||
// to the mesh with a mix of sidecars and gateways until all its instances have a sidecar.
|
// to the mesh with a mix of sidecars and gateways until all its instances have a sidecar.
|
||||||
if connect {
|
if connect {
|
||||||
// Look up gateway nodes associated with the service
|
// Look up gateway nodes associated with the service
|
||||||
nodes, ch, err := s.serviceTerminatingGatewayNodes(tx, serviceName, entMeta)
|
_, nodes, chs, err := s.serviceGatewayNodes(tx, serviceName, structs.ServiceKindTerminatingGateway, entMeta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, fmt.Errorf("failed gateway nodes lookup: %v", err)
|
return 0, nil, fmt.Errorf("failed gateway nodes lookup: %v", err)
|
||||||
}
|
}
|
||||||
ws.Add(ch)
|
|
||||||
|
for _, ch := range chs {
|
||||||
|
ws.Add(ch)
|
||||||
|
}
|
||||||
for i := 0; i < len(nodes); i++ {
|
for i := 0; i < len(nodes); i++ {
|
||||||
results = append(results, nodes[i])
|
results = append(results, nodes[i])
|
||||||
}
|
}
|
||||||
|
@ -1459,18 +1473,12 @@ func (s *Store) deleteServiceTxn(tx *memdb.Txn, idx uint64, nodeName, serviceID
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up association between service name and gateway
|
// Clean up association between service name and gateways
|
||||||
gateway, err := s.serviceTerminatingGateway(tx, svc.ServiceName, &svc.EnterpriseMeta)
|
if _, err := tx.DeleteAll(gatewayServicesTableName, "service", structs.NewServiceID(svc.ServiceName, entMeta)); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("failed to truncate gateway services table: %v", err)
|
||||||
return fmt.Errorf("failed gateway lookup for %q: %s", svc.ServiceName, err)
|
|
||||||
}
|
}
|
||||||
if gateway != nil {
|
if err := indexUpdateMaxTxn(tx, idx, gatewayServicesTableName); err != nil {
|
||||||
if err := tx.Delete(terminatingGatewayServicesTableName, gateway); err != nil {
|
return fmt.Errorf("failed updating gateway-services index: %v", err)
|
||||||
return fmt.Errorf("failed to delete gateway mapping for %q: %v", svc.ServiceName, err)
|
|
||||||
}
|
|
||||||
if err := indexUpdateMaxTxn(tx, idx, terminatingGatewayServicesTableName); err != nil {
|
|
||||||
return fmt.Errorf("failed updating terminating-gateway-services index: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -1942,10 +1950,53 @@ func (s *Store) CheckConnectServiceNodes(ws memdb.WatchSet, serviceName string,
|
||||||
return s.checkServiceNodes(ws, serviceName, true, entMeta)
|
return s.checkServiceNodes(ws, serviceName, true, entMeta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckIngressServiceNodes is used to query all nodes and checks for ingress
|
||||||
|
// endpoints for a given service.
|
||||||
|
func (s *Store) CheckIngressServiceNodes(ws memdb.WatchSet, serviceName string, entMeta *structs.EnterpriseMeta) (uint64, structs.CheckServiceNodes, error) {
|
||||||
|
tx := s.db.Txn(false)
|
||||||
|
defer tx.Abort()
|
||||||
|
maxIdx, nodes, watchChs, err := s.serviceGatewayNodes(tx, serviceName, structs.ServiceKindIngressGateway, entMeta)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, fmt.Errorf("failed gateway nodes lookup: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(ingress) : Deal with incorporating index from mapping table
|
||||||
|
|
||||||
|
// Watch list of gateway nodes for changes
|
||||||
|
for _, ch := range watchChs {
|
||||||
|
ws.Add(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(ingress): Test namespace functionality here
|
||||||
|
// De-dup services to lookup
|
||||||
|
serviceIDs := make(map[structs.ServiceID]struct{})
|
||||||
|
for _, n := range nodes {
|
||||||
|
serviceIDs[n.CompoundServiceName()] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var results structs.CheckServiceNodes
|
||||||
|
for sid := range serviceIDs {
|
||||||
|
idx, n, err := s.checkServiceNodesTxn(tx, ws, sid.ID, false, &sid.EnterpriseMeta)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
if idx > maxIdx {
|
||||||
|
maxIdx = idx
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, n...)
|
||||||
|
}
|
||||||
|
return maxIdx, results, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) checkServiceNodes(ws memdb.WatchSet, serviceName string, connect bool, entMeta *structs.EnterpriseMeta) (uint64, structs.CheckServiceNodes, error) {
|
func (s *Store) checkServiceNodes(ws memdb.WatchSet, serviceName string, connect bool, entMeta *structs.EnterpriseMeta) (uint64, structs.CheckServiceNodes, error) {
|
||||||
tx := s.db.Txn(false)
|
tx := s.db.Txn(false)
|
||||||
defer tx.Abort()
|
defer tx.Abort()
|
||||||
|
|
||||||
|
return s.checkServiceNodesTxn(tx, ws, serviceName, connect, entMeta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) checkServiceNodesTxn(tx *memdb.Txn, ws memdb.WatchSet, serviceName string, connect bool, entMeta *structs.EnterpriseMeta) (uint64, structs.CheckServiceNodes, error) {
|
||||||
// Function for lookup
|
// Function for lookup
|
||||||
index := "service"
|
index := "service"
|
||||||
if connect {
|
if connect {
|
||||||
|
@ -1979,13 +2030,13 @@ func (s *Store) checkServiceNodes(ws memdb.WatchSet, serviceName string, connect
|
||||||
serviceNames[sn.ServiceName] = struct{}{}
|
serviceNames[sn.ServiceName] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we are querying for Connect nodes, the associated proxy might be a gateway.
|
// If we are querying for Connect nodes, the associated proxy might be a terminating-gateway.
|
||||||
// Gateways are tracked in a separate table, and we append them to the result set.
|
// Gateways are tracked in a separate table, and we append them to the result set.
|
||||||
// We append rather than replace since it allows users to migrate a service
|
// We append rather than replace since it allows users to migrate a service
|
||||||
// to the mesh with a mix of sidecars and gateways until all its instances have a sidecar.
|
// to the mesh with a mix of sidecars and gateways until all its instances have a sidecar.
|
||||||
if connect {
|
if connect {
|
||||||
// Look up gateway nodes associated with the service
|
// Look up gateway nodes associated with the service
|
||||||
nodes, _, err := s.serviceTerminatingGatewayNodes(tx, serviceName, entMeta)
|
_, nodes, _, err := s.serviceGatewayNodes(tx, serviceName, structs.ServiceKindTerminatingGateway, entMeta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, fmt.Errorf("failed gateway nodes lookup: %v", err)
|
return 0, nil, fmt.Errorf("failed gateway nodes lookup: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -2095,12 +2146,12 @@ func (s *Store) CheckServiceTagNodes(ws memdb.WatchSet, serviceName string, tags
|
||||||
return s.parseCheckServiceNodes(tx, ws, idx, serviceName, results, err)
|
return s.parseCheckServiceNodes(tx, ws, idx, serviceName, results, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TerminatingGatewayServices is used to query all services associated with a terminating gateway
|
// GatewayServices is used to query all services associated with a gateway
|
||||||
func (s *Store) TerminatingGatewayServices(ws memdb.WatchSet, gateway string, entMeta *structs.EnterpriseMeta) (uint64, structs.GatewayServices, error) {
|
func (s *Store) GatewayServices(ws memdb.WatchSet, gateway string, entMeta *structs.EnterpriseMeta) (uint64, structs.GatewayServices, error) {
|
||||||
tx := s.db.Txn(false)
|
tx := s.db.Txn(false)
|
||||||
defer tx.Abort()
|
defer tx.Abort()
|
||||||
|
|
||||||
iter, err := s.terminatingGatewayServices(tx, gateway, entMeta)
|
iter, err := s.gatewayServices(tx, gateway, entMeta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, fmt.Errorf("failed gateway services lookup: %s", err)
|
return 0, nil, fmt.Errorf("failed gateway services lookup: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -2115,7 +2166,7 @@ func (s *Store) TerminatingGatewayServices(ws memdb.WatchSet, gateway string, en
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
idx := maxIndexTxn(tx, terminatingGatewayServicesTableName)
|
idx := maxIndexTxn(tx, gatewayServicesTableName)
|
||||||
return idx, results, nil
|
return idx, results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2363,79 +2414,127 @@ func checkSessionsTxn(tx *memdb.Txn, hc *structs.HealthCheck) ([]*sessionCheck,
|
||||||
return sessions, nil
|
return sessions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateGatewayService associates services with gateways as specified in a terminating-gateway config entry
|
// updateGatewayServices associates services with gateways as specified in a gateway config entry
|
||||||
func (s *Store) updateTerminatingGatewayServices(tx *memdb.Txn, idx uint64, conf structs.ConfigEntry, entMeta *structs.EnterpriseMeta) error {
|
func (s *Store) updateGatewayServices(tx *memdb.Txn, idx uint64, conf structs.ConfigEntry, entMeta *structs.EnterpriseMeta) error {
|
||||||
entry, ok := conf.(*structs.TerminatingGatewayConfigEntry)
|
var gatewayServices structs.GatewayServices
|
||||||
if !ok {
|
var err error
|
||||||
return fmt.Errorf("unexpected config entry type: %T", conf)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if service list matches the last known list for the config entry, if it does, skip the update
|
gatewayID := structs.NewServiceID(conf.GetName(), conf.GetEnterpriseMeta())
|
||||||
_, c, err := s.configEntryTxn(tx, nil, conf.GetKind(), conf.GetName(), entMeta)
|
switch conf.GetKind() {
|
||||||
if err != nil {
|
case structs.IngressGateway:
|
||||||
return fmt.Errorf("failed to get config entry: %v", err)
|
gatewayServices, err = s.ingressConfigGatewayServices(tx, gatewayID, conf, entMeta)
|
||||||
|
case structs.TerminatingGateway:
|
||||||
|
gatewayServices, err = s.terminatingConfigGatewayServices(tx, gatewayID, conf, entMeta)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("config entry kind %q does not need gateway-services", conf.GetKind())
|
||||||
}
|
}
|
||||||
if cfg, ok := c.(*structs.TerminatingGatewayConfigEntry); ok && cfg != nil {
|
// Return early if there is an error OR we don't have any services to update
|
||||||
if reflect.DeepEqual(cfg.Services, entry.Services) {
|
if err != nil {
|
||||||
// Services are the same, nothing to update
|
return err
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete all associated with gateway first, to avoid keeping mappings that were removed
|
// Delete all associated with gateway first, to avoid keeping mappings that were removed
|
||||||
if _, err := tx.DeleteAll(terminatingGatewayServicesTableName, "gateway", structs.NewServiceID(entry.Name, entMeta)); err != nil {
|
if _, err := tx.DeleteAll(gatewayServicesTableName, "gateway", structs.NewServiceID(conf.GetName(), entMeta)); err != nil {
|
||||||
return fmt.Errorf("failed to truncate gateway services table: %v", err)
|
return fmt.Errorf("failed to truncate gateway services table: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gatewayID := structs.NewServiceID(entry.Name, &entry.EnterpriseMeta)
|
for _, svc := range gatewayServices {
|
||||||
for _, svc := range entry.Services {
|
|
||||||
// If the service is a wildcard we need to target all services within the namespace
|
// If the service is a wildcard we need to target all services within the namespace
|
||||||
if svc.Name == structs.WildcardSpecifier {
|
if svc.Service.ID == structs.WildcardSpecifier {
|
||||||
if err := s.updateTerminatingGatewayNamespace(tx, gatewayID, svc, entMeta); err != nil {
|
if err := s.updateGatewayNamespace(tx, idx, svc, entMeta); err != nil {
|
||||||
return fmt.Errorf("failed to associate gateway %q with wildcard: %v", gatewayID.String(), err)
|
return fmt.Errorf("failed to associate gateway %q with wildcard: %v", gatewayID.String(), err)
|
||||||
}
|
}
|
||||||
// Skip service-specific update below if there was a wildcard update
|
// Skip service-specific update below if there was a wildcard update
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the non-wildcard service is already associated with a gateway
|
|
||||||
existing, err := s.serviceTerminatingGateway(tx, svc.Name, &svc.EnterpriseMeta)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("gateway service lookup failed: %s", err)
|
|
||||||
}
|
|
||||||
if gs, ok := existing.(*structs.GatewayService); ok && gs != nil {
|
|
||||||
// Only return an error if the stored gateway does not match the one from the config entry
|
|
||||||
if !gs.Gateway.Matches(&gatewayID) {
|
|
||||||
return fmt.Errorf("service %q is associated with different gateway, %q", gs.Service.String(), gs.Gateway.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since this service was specified on its own, and not with a wildcard,
|
// Since this service was specified on its own, and not with a wildcard,
|
||||||
// if there is an existing entry, we overwrite it. The service entry is the source of truth.
|
// if there is an existing entry, we overwrite it. The service entry is the source of truth.
|
||||||
//
|
//
|
||||||
// By extension, if TLS creds are provided with a wildcard but are not provided in
|
// By extension, if TLS creds are provided with a wildcard but are not provided in
|
||||||
// the service entry, the service does not inherit the creds from the wildcard.
|
// the service entry, the service does not inherit the creds from the wildcard.
|
||||||
|
err = s.updateGatewayService(tx, idx, svc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := indexUpdateMaxTxn(tx, idx, gatewayServicesTableName); err != nil {
|
||||||
|
return fmt.Errorf("failed updating gateway-services index: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ingressConfigGatewayServices(tx *memdb.Txn, gateway structs.ServiceID, conf structs.ConfigEntry, entMeta *structs.EnterpriseMeta) (structs.GatewayServices, error) {
|
||||||
|
entry, ok := conf.(*structs.IngressGatewayConfigEntry)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected config entry type: %T", conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if service list matches the last known list for the config entry, if it does, skip the update
|
||||||
|
_, c, err := s.configEntryTxn(tx, nil, conf.GetKind(), conf.GetName(), entMeta)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get config entry: %v", err)
|
||||||
|
}
|
||||||
|
if cfg, ok := c.(*structs.IngressGatewayConfigEntry); ok && cfg != nil {
|
||||||
|
if reflect.DeepEqual(cfg.Listeners, entry.Listeners) {
|
||||||
|
// Services are the same, nothing to update
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var gatewayServices structs.GatewayServices
|
||||||
|
for _, listener := range entry.Listeners {
|
||||||
|
for _, service := range listener.Services {
|
||||||
|
mapping := &structs.GatewayService{
|
||||||
|
Gateway: gateway,
|
||||||
|
Service: service.ToServiceID(),
|
||||||
|
GatewayKind: structs.ServiceKindIngressGateway,
|
||||||
|
Port: listener.Port,
|
||||||
|
}
|
||||||
|
|
||||||
|
gatewayServices = append(gatewayServices, mapping)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return gatewayServices, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) terminatingConfigGatewayServices(tx *memdb.Txn, gateway structs.ServiceID, conf structs.ConfigEntry, entMeta *structs.EnterpriseMeta) (structs.GatewayServices, error) {
|
||||||
|
entry, ok := conf.(*structs.TerminatingGatewayConfigEntry)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected config entry type: %T", conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if service list matches the last known list for the config entry, if it does, skip the update
|
||||||
|
_, c, err := s.configEntryTxn(tx, nil, conf.GetKind(), conf.GetName(), entMeta)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get config entry: %v", err)
|
||||||
|
}
|
||||||
|
if cfg, ok := c.(*structs.TerminatingGatewayConfigEntry); ok && cfg != nil {
|
||||||
|
if reflect.DeepEqual(cfg.Services, entry.Services) {
|
||||||
|
// Services are the same, nothing to update
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var gatewayServices structs.GatewayServices
|
||||||
|
for _, svc := range entry.Services {
|
||||||
mapping := &structs.GatewayService{
|
mapping := &structs.GatewayService{
|
||||||
Gateway: gatewayID,
|
Gateway: gateway,
|
||||||
Service: structs.NewServiceID(svc.Name, &svc.EnterpriseMeta),
|
Service: structs.NewServiceID(svc.Name, &svc.EnterpriseMeta),
|
||||||
GatewayKind: structs.ServiceKindTerminatingGateway,
|
GatewayKind: structs.ServiceKindTerminatingGateway,
|
||||||
KeyFile: svc.KeyFile,
|
KeyFile: svc.KeyFile,
|
||||||
CertFile: svc.CertFile,
|
CertFile: svc.CertFile,
|
||||||
CAFile: svc.CAFile,
|
CAFile: svc.CAFile,
|
||||||
}
|
}
|
||||||
if err := tx.Insert(terminatingGatewayServicesTableName, mapping); err != nil {
|
|
||||||
return fmt.Errorf("failed inserting gateway service mapping: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := indexUpdateMaxTxn(tx, idx, terminatingGatewayServicesTableName); err != nil {
|
gatewayServices = append(gatewayServices, mapping)
|
||||||
return fmt.Errorf("failed updating terminating-gateway-services index: %v", err)
|
|
||||||
}
|
}
|
||||||
return nil
|
return gatewayServices, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateTerminatingGatewayNamespace is used to target all services within a namespace with a set of TLS certificates
|
// updateGatewayNamespace is used to target all services within a namespace
|
||||||
func (s *Store) updateTerminatingGatewayNamespace(tx *memdb.Txn, gateway structs.ServiceID, service structs.LinkedService, entMeta *structs.EnterpriseMeta) error {
|
func (s *Store) updateGatewayNamespace(tx *memdb.Txn, idx uint64, service *structs.GatewayService, entMeta *structs.EnterpriseMeta) error {
|
||||||
services, err := s.catalogServiceListByKind(tx, structs.ServiceKindTypical, entMeta)
|
services, err := s.catalogServiceListByKind(tx, structs.ServiceKindTypical, entMeta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed querying services: %s", err)
|
return fmt.Errorf("failed querying services: %s", err)
|
||||||
|
@ -2450,125 +2549,108 @@ func (s *Store) updateTerminatingGatewayNamespace(tx *memdb.Txn, gateway structs
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
existing, err := s.serviceTerminatingGateway(tx, sn.ServiceName, &sn.EnterpriseMeta)
|
existing, err := tx.First(gatewayServicesTableName, "id", service.Gateway, sn.CompoundServiceName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("gateway service lookup failed: %s", err)
|
return fmt.Errorf("gateway service lookup failed: %s", err)
|
||||||
}
|
}
|
||||||
|
if existing != nil {
|
||||||
if gs, ok := existing.(*structs.GatewayService); ok && gs != nil {
|
|
||||||
// Return an error if the wildcard is attempting to cover a service specified by a different gateway's config entry
|
|
||||||
if !gs.Gateway.Matches(&gateway) {
|
|
||||||
return fmt.Errorf("service %q is associated with different gateway, %q", gs.Service.String(), gs.Gateway.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's an existing service associated with this gateway then we skip it.
|
// If there's an existing service associated with this gateway then we skip it.
|
||||||
// This means the service was specified on its own, and the service entry overrides the wildcard entry.
|
// This means the service was specified on its own, and the service entry overrides the wildcard entry.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
mapping := &structs.GatewayService{
|
mapping := service.Clone()
|
||||||
Gateway: gateway,
|
mapping.Service = structs.NewServiceID(sn.ServiceName, &service.Service.EnterpriseMeta)
|
||||||
Service: structs.NewServiceID(sn.ServiceName, &service.EnterpriseMeta),
|
err = s.updateGatewayService(tx, idx, mapping)
|
||||||
GatewayKind: structs.ServiceKindTerminatingGateway,
|
if err != nil {
|
||||||
KeyFile: service.KeyFile,
|
return err
|
||||||
CertFile: service.CertFile,
|
|
||||||
CAFile: service.CAFile,
|
|
||||||
}
|
|
||||||
if err := tx.Insert(terminatingGatewayServicesTableName, mapping); err != nil {
|
|
||||||
return fmt.Errorf("failed inserting gateway service mapping: %s", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also store a mapping for the wildcard so that the TLS creds can be pulled
|
// Also store a mapping for the wildcard so that the TLS creds can be pulled
|
||||||
// for new services registered in its namespace
|
// for new services registered in its namespace
|
||||||
mapping := &structs.GatewayService{
|
err = s.updateGatewayService(tx, idx, service)
|
||||||
Gateway: gateway,
|
if err != nil {
|
||||||
Service: structs.NewServiceID(service.Name, &service.EnterpriseMeta),
|
return err
|
||||||
GatewayKind: structs.ServiceKindTerminatingGateway,
|
|
||||||
KeyFile: service.KeyFile,
|
|
||||||
CertFile: service.CertFile,
|
|
||||||
CAFile: service.CAFile,
|
|
||||||
}
|
|
||||||
if err := tx.Insert(terminatingGatewayServicesTableName, mapping); err != nil {
|
|
||||||
return fmt.Errorf("failed inserting gateway service mapping: %s", err)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateGatewayService associates services with gateways after an eligible event
|
// updateGatewayService associates services with gateways after an eligible event
|
||||||
// ie. Registering a service in a namespace targeted by a gateway
|
// ie. Registering a service in a namespace targeted by a gateway
|
||||||
func (s *Store) updateTerminatingGatewayService(tx *memdb.Txn, idx uint64, gateway structs.ServiceID, service string, entMeta *structs.EnterpriseMeta) error {
|
func (s *Store) updateGatewayService(tx *memdb.Txn, idx uint64, mapping *structs.GatewayService) error {
|
||||||
mapping := &structs.GatewayService{
|
|
||||||
Gateway: gateway,
|
|
||||||
Service: structs.NewServiceID(service, entMeta),
|
|
||||||
GatewayKind: structs.ServiceKindTerminatingGateway,
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a wildcard specifier is registered for that namespace, use its TLS config
|
|
||||||
wc, err := s.serviceTerminatingGateway(tx, structs.WildcardSpecifier, entMeta)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("gateway service lookup failed: %s", err)
|
|
||||||
}
|
|
||||||
if wc != nil {
|
|
||||||
cfg := wc.(*structs.GatewayService)
|
|
||||||
mapping.CAFile = cfg.CAFile
|
|
||||||
mapping.CertFile = cfg.CertFile
|
|
||||||
mapping.KeyFile = cfg.KeyFile
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if mapping already exists in table if it's already in the table
|
// Check if mapping already exists in table if it's already in the table
|
||||||
// Avoid insert if nothing changed
|
// Avoid insert if nothing changed
|
||||||
existing, err := s.serviceTerminatingGateway(tx, service, entMeta)
|
existing, err := tx.First(gatewayServicesTableName, "id", mapping.Gateway, mapping.Service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("gateway service lookup failed: %s", err)
|
return fmt.Errorf("gateway service lookup failed: %s", err)
|
||||||
}
|
}
|
||||||
if gs, ok := existing.(*structs.GatewayService); ok && gs != nil {
|
if gs, ok := existing.(*structs.GatewayService); ok && gs != nil {
|
||||||
|
mapping.CreateIndex = gs.CreateIndex
|
||||||
if gs.IsSame(mapping) {
|
if gs.IsSame(mapping) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// We have a new mapping
|
||||||
|
mapping.CreateIndex = idx
|
||||||
}
|
}
|
||||||
|
mapping.ModifyIndex = idx
|
||||||
|
|
||||||
if err := tx.Insert(terminatingGatewayServicesTableName, mapping); err != nil {
|
if err := tx.Insert(gatewayServicesTableName, mapping); err != nil {
|
||||||
return fmt.Errorf("failed inserting gateway service mapping: %s", err)
|
return fmt.Errorf("failed inserting gateway service mapping: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := indexUpdateMaxTxn(tx, idx, terminatingGatewayServicesTableName); err != nil {
|
if err := indexUpdateMaxTxn(tx, idx, gatewayServicesTableName); err != nil {
|
||||||
return fmt.Errorf("failed updating terminating-gateway-services index: %v", err)
|
return fmt.Errorf("failed updating gateway-services index: %v", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) serviceTerminatingGateway(tx *memdb.Txn, name string, entMeta *structs.EnterpriseMeta) (interface{}, error) {
|
// serviceGateways returns all GatewayService entries with the given service name. This effectively looks up
|
||||||
return tx.First(terminatingGatewayServicesTableName, "service", structs.NewServiceID(name, entMeta))
|
// all the gateways mapped to this service.
|
||||||
|
func (s *Store) serviceGateways(tx *memdb.Txn, name string, entMeta *structs.EnterpriseMeta) (memdb.ResultIterator, error) {
|
||||||
|
return tx.Get(gatewayServicesTableName, "service", structs.NewServiceID(name, entMeta))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) terminatingGatewayServices(tx *memdb.Txn, name string, entMeta *structs.EnterpriseMeta) (memdb.ResultIterator, error) {
|
func (s *Store) gatewayServices(tx *memdb.Txn, name string, entMeta *structs.EnterpriseMeta) (memdb.ResultIterator, error) {
|
||||||
return tx.Get(terminatingGatewayServicesTableName, "gateway", structs.NewServiceID(name, entMeta))
|
return tx.Get(gatewayServicesTableName, "gateway", structs.NewServiceID(name, entMeta))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) serviceTerminatingGatewayNodes(tx *memdb.Txn, service string, entMeta *structs.EnterpriseMeta) (structs.ServiceNodes, <-chan struct{}, error) {
|
// TODO(ingress): How to handle index rolling back when a config entry is
|
||||||
|
// deleted that references a service?
|
||||||
|
// We might need something like the service_last_extinction index?
|
||||||
|
func (s *Store) serviceGatewayNodes(tx *memdb.Txn, service string, kind structs.ServiceKind, entMeta *structs.EnterpriseMeta) (uint64, structs.ServiceNodes, []<-chan struct{}, error) {
|
||||||
// Look up gateway name associated with the service
|
// Look up gateway name associated with the service
|
||||||
gw, err := s.serviceTerminatingGateway(tx, service, entMeta)
|
gws, err := s.serviceGateways(tx, service, entMeta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed gateway lookup: %s", err)
|
return 0, nil, nil, fmt.Errorf("failed gateway lookup: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var ret structs.ServiceNodes
|
var ret structs.ServiceNodes
|
||||||
var watchChan <-chan struct{}
|
var watchChans []<-chan struct{}
|
||||||
|
var maxIdx uint64
|
||||||
|
|
||||||
if gw != nil {
|
for gateway := gws.Next(); gateway != nil; gateway = gws.Next() {
|
||||||
mapping := gw.(*structs.GatewayService)
|
mapping := gateway.(*structs.GatewayService)
|
||||||
|
// TODO(ingress): Test this conditional
|
||||||
|
if mapping.GatewayKind != kind {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if mapping.ModifyIndex > maxIdx {
|
||||||
|
maxIdx = mapping.ModifyIndex
|
||||||
|
}
|
||||||
|
|
||||||
// Look up nodes for gateway
|
// Look up nodes for gateway
|
||||||
gateways, err := s.catalogServiceNodeList(tx, mapping.Gateway.ID, "service", &mapping.Gateway.EnterpriseMeta)
|
gwServices, err := s.catalogServiceNodeList(tx, mapping.Gateway.ID, "service", &mapping.Gateway.EnterpriseMeta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed service lookup: %s", err)
|
return 0, nil, nil, fmt.Errorf("failed service lookup: %s", err)
|
||||||
}
|
}
|
||||||
for gateway := gateways.Next(); gateway != nil; gateway = gateways.Next() {
|
for svc := gwServices.Next(); svc != nil; svc = gwServices.Next() {
|
||||||
sn := gateway.(*structs.ServiceNode)
|
sn := svc.(*structs.ServiceNode)
|
||||||
ret = append(ret, sn)
|
ret = append(ret, sn)
|
||||||
}
|
}
|
||||||
watchChan = gateways.WatchCh()
|
watchChans = append(watchChans, gwServices.WatchCh())
|
||||||
}
|
}
|
||||||
return ret, watchChan, nil
|
return maxIdx, ret, watchChans, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -4383,12 +4383,12 @@ func TestStateStore_ensureServiceCASTxn(t *testing.T) {
|
||||||
tx.Commit()
|
tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStateStore_TerminatingGatewayServices(t *testing.T) {
|
func TestStateStore_GatewayServices_Terminating(t *testing.T) {
|
||||||
s := testStateStore(t)
|
s := testStateStore(t)
|
||||||
|
|
||||||
// Listing with no results returns an empty list.
|
// Listing with no results returns an empty list.
|
||||||
ws := memdb.NewWatchSet()
|
ws := memdb.NewWatchSet()
|
||||||
idx, nodes, err := s.TerminatingGatewayServices(ws, "db", nil)
|
idx, nodes, err := s.GatewayServices(ws, "db", nil)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, idx, uint64(0))
|
assert.Equal(t, idx, uint64(0))
|
||||||
assert.Len(t, nodes, 0)
|
assert.Len(t, nodes, 0)
|
||||||
|
@ -4444,7 +4444,7 @@ func TestStateStore_TerminatingGatewayServices(t *testing.T) {
|
||||||
|
|
||||||
// Read everything back.
|
// Read everything back.
|
||||||
ws = memdb.NewWatchSet()
|
ws = memdb.NewWatchSet()
|
||||||
idx, out, err := s.TerminatingGatewayServices(ws, "gateway", nil)
|
idx, out, err := s.GatewayServices(ws, "gateway", nil)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, idx, uint64(21))
|
assert.Equal(t, idx, uint64(21))
|
||||||
assert.Len(t, out, 2)
|
assert.Len(t, out, 2)
|
||||||
|
@ -4454,11 +4454,19 @@ func TestStateStore_TerminatingGatewayServices(t *testing.T) {
|
||||||
Service: structs.NewServiceID("api", nil),
|
Service: structs.NewServiceID("api", nil),
|
||||||
Gateway: structs.NewServiceID("gateway", nil),
|
Gateway: structs.NewServiceID("gateway", nil),
|
||||||
GatewayKind: structs.ServiceKindTerminatingGateway,
|
GatewayKind: structs.ServiceKindTerminatingGateway,
|
||||||
|
RaftIndex: structs.RaftIndex{
|
||||||
|
CreateIndex: 21,
|
||||||
|
ModifyIndex: 21,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Service: structs.NewServiceID("db", nil),
|
Service: structs.NewServiceID("db", nil),
|
||||||
Gateway: structs.NewServiceID("gateway", nil),
|
Gateway: structs.NewServiceID("gateway", nil),
|
||||||
GatewayKind: structs.ServiceKindTerminatingGateway,
|
GatewayKind: structs.ServiceKindTerminatingGateway,
|
||||||
|
RaftIndex: structs.RaftIndex{
|
||||||
|
CreateIndex: 21,
|
||||||
|
ModifyIndex: 21,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
assert.Equal(t, expect, out)
|
assert.Equal(t, expect, out)
|
||||||
|
@ -4489,7 +4497,7 @@ func TestStateStore_TerminatingGatewayServices(t *testing.T) {
|
||||||
|
|
||||||
// Read everything back.
|
// Read everything back.
|
||||||
ws = memdb.NewWatchSet()
|
ws = memdb.NewWatchSet()
|
||||||
idx, out, err = s.TerminatingGatewayServices(ws, "gateway", nil)
|
idx, out, err = s.GatewayServices(ws, "gateway", nil)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, idx, uint64(22))
|
assert.Equal(t, idx, uint64(22))
|
||||||
assert.Len(t, out, 2)
|
assert.Len(t, out, 2)
|
||||||
|
@ -4502,11 +4510,19 @@ func TestStateStore_TerminatingGatewayServices(t *testing.T) {
|
||||||
CAFile: "api/ca.crt",
|
CAFile: "api/ca.crt",
|
||||||
CertFile: "api/client.crt",
|
CertFile: "api/client.crt",
|
||||||
KeyFile: "api/client.key",
|
KeyFile: "api/client.key",
|
||||||
|
RaftIndex: structs.RaftIndex{
|
||||||
|
CreateIndex: 22,
|
||||||
|
ModifyIndex: 22,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Service: structs.NewServiceID("db", nil),
|
Service: structs.NewServiceID("db", nil),
|
||||||
Gateway: structs.NewServiceID("gateway", nil),
|
Gateway: structs.NewServiceID("gateway", nil),
|
||||||
GatewayKind: structs.ServiceKindTerminatingGateway,
|
GatewayKind: structs.ServiceKindTerminatingGateway,
|
||||||
|
RaftIndex: structs.RaftIndex{
|
||||||
|
CreateIndex: 22,
|
||||||
|
ModifyIndex: 22,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
assert.Equal(t, expect, out)
|
assert.Equal(t, expect, out)
|
||||||
|
@ -4515,7 +4531,7 @@ func TestStateStore_TerminatingGatewayServices(t *testing.T) {
|
||||||
assert.Nil(t, s.EnsureService(23, "bar", &structs.NodeService{ID: "redis", Service: "redis", Tags: nil, Address: "", Port: 6379}))
|
assert.Nil(t, s.EnsureService(23, "bar", &structs.NodeService{ID: "redis", Service: "redis", Tags: nil, Address: "", Port: 6379}))
|
||||||
assert.True(t, watchFired(ws))
|
assert.True(t, watchFired(ws))
|
||||||
|
|
||||||
idx, out, err = s.TerminatingGatewayServices(ws, "gateway", nil)
|
idx, out, err = s.GatewayServices(ws, "gateway", nil)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, idx, uint64(23))
|
assert.Equal(t, idx, uint64(23))
|
||||||
assert.Len(t, out, 3)
|
assert.Len(t, out, 3)
|
||||||
|
@ -4528,11 +4544,19 @@ func TestStateStore_TerminatingGatewayServices(t *testing.T) {
|
||||||
CAFile: "api/ca.crt",
|
CAFile: "api/ca.crt",
|
||||||
CertFile: "api/client.crt",
|
CertFile: "api/client.crt",
|
||||||
KeyFile: "api/client.key",
|
KeyFile: "api/client.key",
|
||||||
|
RaftIndex: structs.RaftIndex{
|
||||||
|
CreateIndex: 22,
|
||||||
|
ModifyIndex: 22,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Service: structs.NewServiceID("db", nil),
|
Service: structs.NewServiceID("db", nil),
|
||||||
Gateway: structs.NewServiceID("gateway", nil),
|
Gateway: structs.NewServiceID("gateway", nil),
|
||||||
GatewayKind: structs.ServiceKindTerminatingGateway,
|
GatewayKind: structs.ServiceKindTerminatingGateway,
|
||||||
|
RaftIndex: structs.RaftIndex{
|
||||||
|
CreateIndex: 22,
|
||||||
|
ModifyIndex: 22,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Service: structs.NewServiceID("redis", nil),
|
Service: structs.NewServiceID("redis", nil),
|
||||||
|
@ -4541,6 +4565,10 @@ func TestStateStore_TerminatingGatewayServices(t *testing.T) {
|
||||||
CAFile: "ca.crt",
|
CAFile: "ca.crt",
|
||||||
CertFile: "client.crt",
|
CertFile: "client.crt",
|
||||||
KeyFile: "client.key",
|
KeyFile: "client.key",
|
||||||
|
RaftIndex: structs.RaftIndex{
|
||||||
|
CreateIndex: 23,
|
||||||
|
ModifyIndex: 23,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
assert.Equal(t, expect, out)
|
assert.Equal(t, expect, out)
|
||||||
|
@ -4549,7 +4577,7 @@ func TestStateStore_TerminatingGatewayServices(t *testing.T) {
|
||||||
assert.Nil(t, s.DeleteService(24, "bar", "redis", nil))
|
assert.Nil(t, s.DeleteService(24, "bar", "redis", nil))
|
||||||
assert.True(t, watchFired(ws))
|
assert.True(t, watchFired(ws))
|
||||||
|
|
||||||
idx, out, err = s.TerminatingGatewayServices(ws, "gateway", nil)
|
idx, out, err = s.GatewayServices(ws, "gateway", nil)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, idx, uint64(24))
|
assert.Equal(t, idx, uint64(24))
|
||||||
assert.Len(t, out, 2)
|
assert.Len(t, out, 2)
|
||||||
|
@ -4562,16 +4590,24 @@ func TestStateStore_TerminatingGatewayServices(t *testing.T) {
|
||||||
CAFile: "api/ca.crt",
|
CAFile: "api/ca.crt",
|
||||||
CertFile: "api/client.crt",
|
CertFile: "api/client.crt",
|
||||||
KeyFile: "api/client.key",
|
KeyFile: "api/client.key",
|
||||||
|
RaftIndex: structs.RaftIndex{
|
||||||
|
CreateIndex: 22,
|
||||||
|
ModifyIndex: 22,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Service: structs.NewServiceID("db", nil),
|
Service: structs.NewServiceID("db", nil),
|
||||||
Gateway: structs.NewServiceID("gateway", nil),
|
Gateway: structs.NewServiceID("gateway", nil),
|
||||||
GatewayKind: structs.ServiceKindTerminatingGateway,
|
GatewayKind: structs.ServiceKindTerminatingGateway,
|
||||||
|
RaftIndex: structs.RaftIndex{
|
||||||
|
CreateIndex: 22,
|
||||||
|
ModifyIndex: 22,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
assert.Equal(t, expect, out)
|
assert.Equal(t, expect, out)
|
||||||
|
|
||||||
// Create a new entry that only leaves one service
|
// Update the entry that only leaves one service
|
||||||
assert.Nil(t, s.EnsureConfigEntry(25, &structs.TerminatingGatewayConfigEntry{
|
assert.Nil(t, s.EnsureConfigEntry(25, &structs.TerminatingGatewayConfigEntry{
|
||||||
Kind: "terminating-gateway",
|
Kind: "terminating-gateway",
|
||||||
Name: "gateway",
|
Name: "gateway",
|
||||||
|
@ -4583,7 +4619,7 @@ func TestStateStore_TerminatingGatewayServices(t *testing.T) {
|
||||||
}, nil))
|
}, nil))
|
||||||
assert.True(t, watchFired(ws))
|
assert.True(t, watchFired(ws))
|
||||||
|
|
||||||
idx, out, err = s.TerminatingGatewayServices(ws, "gateway", nil)
|
idx, out, err = s.GatewayServices(ws, "gateway", nil)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, idx, uint64(25))
|
assert.Equal(t, idx, uint64(25))
|
||||||
assert.Len(t, out, 1)
|
assert.Len(t, out, 1)
|
||||||
|
@ -4594,12 +4630,16 @@ func TestStateStore_TerminatingGatewayServices(t *testing.T) {
|
||||||
Service: structs.NewServiceID("db", nil),
|
Service: structs.NewServiceID("db", nil),
|
||||||
Gateway: structs.NewServiceID("gateway", nil),
|
Gateway: structs.NewServiceID("gateway", nil),
|
||||||
GatewayKind: structs.ServiceKindTerminatingGateway,
|
GatewayKind: structs.ServiceKindTerminatingGateway,
|
||||||
|
RaftIndex: structs.RaftIndex{
|
||||||
|
CreateIndex: 25,
|
||||||
|
ModifyIndex: 25,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
assert.Equal(t, expect, out)
|
assert.Equal(t, expect, out)
|
||||||
|
|
||||||
// Attempt to associate a different gateway with services that include db
|
// Attempt to associate a different gateway with services that include db
|
||||||
assert.Error(t, s.EnsureConfigEntry(26, &structs.TerminatingGatewayConfigEntry{
|
assert.Nil(t, s.EnsureConfigEntry(26, &structs.TerminatingGatewayConfigEntry{
|
||||||
Kind: "terminating-gateway",
|
Kind: "terminating-gateway",
|
||||||
Name: "gateway2",
|
Name: "gateway2",
|
||||||
Services: []structs.LinkedService{
|
Services: []structs.LinkedService{
|
||||||
|
@ -4607,14 +4647,307 @@ func TestStateStore_TerminatingGatewayServices(t *testing.T) {
|
||||||
Name: "*",
|
Name: "*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, nil), "service \"db\" is associated with different gateway")
|
}, nil))
|
||||||
|
|
||||||
// Deleting the config entry should remove existing mappings
|
idx, out, err = s.GatewayServices(ws, "gateway2", nil)
|
||||||
assert.Nil(t, s.DeleteConfigEntry(26, "terminating-gateway", "gateway", nil))
|
|
||||||
assert.True(t, watchFired(ws))
|
|
||||||
|
|
||||||
idx, out, err = s.TerminatingGatewayServices(ws, "gateway", nil)
|
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, idx, uint64(26))
|
assert.Equal(t, idx, uint64(26))
|
||||||
|
assert.Len(t, out, 2)
|
||||||
|
|
||||||
|
expect = structs.GatewayServices{
|
||||||
|
{
|
||||||
|
Service: structs.NewServiceID("api", nil),
|
||||||
|
Gateway: structs.NewServiceID("gateway2", nil),
|
||||||
|
GatewayKind: structs.ServiceKindTerminatingGateway,
|
||||||
|
RaftIndex: structs.RaftIndex{
|
||||||
|
CreateIndex: 26,
|
||||||
|
ModifyIndex: 26,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Service: structs.NewServiceID("db", nil),
|
||||||
|
Gateway: structs.NewServiceID("gateway2", nil),
|
||||||
|
GatewayKind: structs.ServiceKindTerminatingGateway,
|
||||||
|
RaftIndex: structs.RaftIndex{
|
||||||
|
CreateIndex: 26,
|
||||||
|
ModifyIndex: 26,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Equal(t, expect, out)
|
||||||
|
|
||||||
|
// Deleting the config entry should remove existing mappings
|
||||||
|
assert.Nil(t, s.DeleteConfigEntry(27, "terminating-gateway", "gateway", nil))
|
||||||
|
assert.True(t, watchFired(ws))
|
||||||
|
|
||||||
|
idx, out, err = s.GatewayServices(ws, "gateway", nil)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, idx, uint64(27))
|
||||||
assert.Len(t, out, 0)
|
assert.Len(t, out, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStateStore_CheckIngressServiceNodes(t *testing.T) {
|
||||||
|
s := testStateStore(t)
|
||||||
|
ws := setupIngressState(t, s)
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
t.Run("check service1 ingress gateway", func(t *testing.T) {
|
||||||
|
idx, results, err := s.CheckIngressServiceNodes(ws, "service1", nil)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(uint64(13), idx)
|
||||||
|
// Multiple instances of the ingress2 service
|
||||||
|
require.Len(results, 4)
|
||||||
|
|
||||||
|
ids := make(map[string]struct{})
|
||||||
|
for _, n := range results {
|
||||||
|
ids[n.Service.ID] = struct{}{}
|
||||||
|
}
|
||||||
|
expectedIds := map[string]struct{}{
|
||||||
|
"ingress1": struct{}{},
|
||||||
|
"ingress2": struct{}{},
|
||||||
|
"wildcardIngress": struct{}{},
|
||||||
|
}
|
||||||
|
require.Equal(expectedIds, ids)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("check service2 ingress gateway", func(t *testing.T) {
|
||||||
|
idx, results, err := s.CheckIngressServiceNodes(ws, "service2", nil)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(uint64(12), idx)
|
||||||
|
require.Len(results, 2)
|
||||||
|
|
||||||
|
ids := make(map[string]struct{})
|
||||||
|
for _, n := range results {
|
||||||
|
ids[n.Service.ID] = struct{}{}
|
||||||
|
}
|
||||||
|
expectedIds := map[string]struct{}{
|
||||||
|
"ingress1": struct{}{},
|
||||||
|
"wildcardIngress": struct{}{},
|
||||||
|
}
|
||||||
|
require.Equal(expectedIds, ids)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("check service3 ingress gateway", func(t *testing.T) {
|
||||||
|
idx, results, err := s.CheckIngressServiceNodes(ws, "service3", nil)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(uint64(11), idx)
|
||||||
|
require.Len(results, 1)
|
||||||
|
require.Equal("wildcardIngress", results[0].Service.ID)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("delete a wildcard entry", func(t *testing.T) {
|
||||||
|
require.Nil(s.DeleteConfigEntry(19, "ingress-gateway", "wildcardIngress", nil))
|
||||||
|
require.True(watchFired(ws))
|
||||||
|
idx, results, err := s.CheckIngressServiceNodes(ws, "service1", nil)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(uint64(13), idx)
|
||||||
|
require.Len(results, 3)
|
||||||
|
|
||||||
|
idx, results, err = s.CheckIngressServiceNodes(ws, "service2", nil)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(uint64(12), idx)
|
||||||
|
require.Len(results, 1)
|
||||||
|
|
||||||
|
idx, results, err = s.CheckIngressServiceNodes(ws, "service3", nil)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(uint64(0), idx)
|
||||||
|
// TODO(ingress): index goes backward when deleting last config entry
|
||||||
|
// require.Equal(uint64(11), idx)
|
||||||
|
require.Len(results, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStateStore_GatewayServices_Ingress(t *testing.T) {
|
||||||
|
s := testStateStore(t)
|
||||||
|
ws := setupIngressState(t, s)
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
t.Run("ingress1 gateway services", func(t *testing.T) {
|
||||||
|
idx, results, err := s.GatewayServices(ws, "ingress1", nil)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(uint64(14), idx)
|
||||||
|
require.Len(results, 2)
|
||||||
|
require.Equal("ingress1", results[0].Gateway.ID)
|
||||||
|
require.Equal("service1", results[0].Service.ID)
|
||||||
|
require.Equal(1111, results[0].Port)
|
||||||
|
require.Equal("ingress1", results[1].Gateway.ID)
|
||||||
|
require.Equal("service2", results[1].Service.ID)
|
||||||
|
require.Equal(2222, results[1].Port)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ingress2 gateway services", func(t *testing.T) {
|
||||||
|
idx, results, err := s.GatewayServices(ws, "ingress2", nil)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(uint64(14), idx)
|
||||||
|
require.Len(results, 1)
|
||||||
|
require.Equal("ingress2", results[0].Gateway.ID)
|
||||||
|
require.Equal("service1", results[0].Service.ID)
|
||||||
|
require.Equal(3333, results[0].Port)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("No gatway services associated", func(t *testing.T) {
|
||||||
|
idx, results, err := s.GatewayServices(ws, "nothingIngress", nil)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(uint64(14), idx)
|
||||||
|
require.Len(results, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("wildcard gateway services", func(t *testing.T) {
|
||||||
|
idx, results, err := s.GatewayServices(ws, "wildcardIngress", nil)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(uint64(14), idx)
|
||||||
|
require.Len(results, 3)
|
||||||
|
require.Equal("wildcardIngress", results[0].Gateway.ID)
|
||||||
|
require.Equal("service1", results[0].Service.ID)
|
||||||
|
require.Equal(4444, results[0].Port)
|
||||||
|
require.Equal("wildcardIngress", results[1].Gateway.ID)
|
||||||
|
require.Equal("service2", results[1].Service.ID)
|
||||||
|
require.Equal(4444, results[1].Port)
|
||||||
|
require.Equal("wildcardIngress", results[2].Gateway.ID)
|
||||||
|
require.Equal("service3", results[2].Service.ID)
|
||||||
|
require.Equal(4444, results[2].Port)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("deregistering a service", func(t *testing.T) {
|
||||||
|
require.Nil(s.DeleteService(18, "node1", "service1", nil))
|
||||||
|
require.True(watchFired(ws))
|
||||||
|
idx, results, err := s.GatewayServices(ws, "wildcardIngress", nil)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(uint64(18), idx)
|
||||||
|
require.Len(results, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO(ingress): This test case fails right now because of a
|
||||||
|
// bug in DeleteService where we delete are entries associated
|
||||||
|
// to a service, not just an entry created by a wildcard.
|
||||||
|
// t.Run("check ingress2 gateway services again", func(t *testing.T) {
|
||||||
|
// idx, results, err := s.GatewayServices(ws, "ingress2", nil)
|
||||||
|
// require.NoError(err)
|
||||||
|
// require.Equal(uint64(18), idx)
|
||||||
|
// require.Len(results, 1)
|
||||||
|
// require.Equal("ingress2", results[0].Gateway.ID)
|
||||||
|
// require.Equal("service1", results[0].Service.ID)
|
||||||
|
// require.Equal(3333, results[0].Port)
|
||||||
|
// })
|
||||||
|
|
||||||
|
t.Run("deleting a wildcard config entry", func(t *testing.T) {
|
||||||
|
require.Nil(s.DeleteConfigEntry(19, "ingress-gateway", "wildcardIngress", nil))
|
||||||
|
require.True(watchFired(ws))
|
||||||
|
idx, results, err := s.GatewayServices(ws, "wildcardIngress", nil)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(uint64(19), idx)
|
||||||
|
require.Len(results, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("updating a config entry with zero listeners", func(t *testing.T) {
|
||||||
|
ingress1 := &structs.IngressGatewayConfigEntry{
|
||||||
|
Kind: "ingress-gateway",
|
||||||
|
Name: "ingress1",
|
||||||
|
Listeners: []structs.IngressListener{},
|
||||||
|
}
|
||||||
|
require.Nil(s.EnsureConfigEntry(20, ingress1, nil))
|
||||||
|
require.True(watchFired(ws))
|
||||||
|
idx, results, err := s.GatewayServices(ws, "ingress1", nil)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(uint64(20), idx)
|
||||||
|
require.Len(results, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupIngressState(t *testing.T, s *Store) memdb.WatchSet {
|
||||||
|
// Querying with no matches gives an empty response
|
||||||
|
ws := memdb.NewWatchSet()
|
||||||
|
idx, res, err := s.GatewayServices(ws, "ingress1", nil)
|
||||||
|
if idx != 0 || res != nil || err != nil {
|
||||||
|
t.Fatalf("expected (0, nil, nil), got: (%d, %#v, %#v)", idx, res, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register some nodes.
|
||||||
|
testRegisterNode(t, s, 0, "node1")
|
||||||
|
testRegisterNode(t, s, 1, "node2")
|
||||||
|
|
||||||
|
// Register a service against the nodes.
|
||||||
|
testRegisterIngressService(t, s, 3, "node1", "wildcardIngress")
|
||||||
|
testRegisterIngressService(t, s, 4, "node1", "ingress1")
|
||||||
|
testRegisterIngressService(t, s, 5, "node1", "ingress2")
|
||||||
|
testRegisterIngressService(t, s, 6, "node2", "ingress2")
|
||||||
|
testRegisterIngressService(t, s, 7, "node1", "nothingIngress")
|
||||||
|
testRegisterService(t, s, 8, "node1", "service1")
|
||||||
|
testRegisterService(t, s, 9, "node2", "service2")
|
||||||
|
testRegisterService(t, s, 10, "node2", "service3")
|
||||||
|
|
||||||
|
// Register some ingress config entries.
|
||||||
|
|
||||||
|
wildcardIngress := &structs.IngressGatewayConfigEntry{
|
||||||
|
Kind: "ingress-gateway",
|
||||||
|
Name: "wildcardIngress",
|
||||||
|
Listeners: []structs.IngressListener{
|
||||||
|
{
|
||||||
|
Port: 4444,
|
||||||
|
Protocol: "tcp",
|
||||||
|
Services: []structs.IngressService{
|
||||||
|
{
|
||||||
|
Name: "*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.NoError(t, s.EnsureConfigEntry(11, wildcardIngress, nil))
|
||||||
|
|
||||||
|
assert.True(t, watchFired(ws))
|
||||||
|
ingress1 := &structs.IngressGatewayConfigEntry{
|
||||||
|
Kind: "ingress-gateway",
|
||||||
|
Name: "ingress1",
|
||||||
|
Listeners: []structs.IngressListener{
|
||||||
|
{
|
||||||
|
Port: 1111,
|
||||||
|
Protocol: "tcp",
|
||||||
|
Services: []structs.IngressService{
|
||||||
|
{
|
||||||
|
Name: "service1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Port: 2222,
|
||||||
|
Protocol: "tcp",
|
||||||
|
Services: []structs.IngressService{
|
||||||
|
{
|
||||||
|
Name: "service2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.NoError(t, s.EnsureConfigEntry(12, ingress1, nil))
|
||||||
|
assert.True(t, watchFired(ws))
|
||||||
|
|
||||||
|
ingress2 := &structs.IngressGatewayConfigEntry{
|
||||||
|
Kind: "ingress-gateway",
|
||||||
|
Name: "ingress2",
|
||||||
|
Listeners: []structs.IngressListener{
|
||||||
|
{
|
||||||
|
Port: 3333,
|
||||||
|
Protocol: "tcp",
|
||||||
|
Services: []structs.IngressService{
|
||||||
|
{
|
||||||
|
Name: "service1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.NoError(t, s.EnsureConfigEntry(13, ingress2, nil))
|
||||||
|
assert.True(t, watchFired(ws))
|
||||||
|
|
||||||
|
nothingIngress := &structs.IngressGatewayConfigEntry{
|
||||||
|
Kind: "ingress-gateway",
|
||||||
|
Name: "nothingIngress",
|
||||||
|
Listeners: []structs.IngressListener{},
|
||||||
|
}
|
||||||
|
assert.NoError(t, s.EnsureConfigEntry(14, nothingIngress, nil))
|
||||||
|
assert.True(t, watchFired(ws))
|
||||||
|
|
||||||
|
return ws
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/consul/discoverychain"
|
"github.com/hashicorp/consul/agent/consul/discoverychain"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
memdb "github.com/hashicorp/go-memdb"
|
memdb "github.com/hashicorp/go-memdb"
|
||||||
|
@ -214,10 +215,10 @@ func (s *Store) ensureConfigEntryTxn(tx *memdb.Txn, idx uint64, conf structs.Con
|
||||||
return err // Err is already sufficiently decorated.
|
return err // Err is already sufficiently decorated.
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the config entry is for terminating gateways we update the memdb table
|
// If the config entry is for a terminating or ingress gateway we update the memdb table
|
||||||
// that associates gateways <-> services.
|
// that associates gateways <-> services.
|
||||||
if conf.GetKind() == structs.TerminatingGateway {
|
if conf.GetKind() == structs.TerminatingGateway || conf.GetKind() == structs.IngressGateway {
|
||||||
err = s.updateTerminatingGatewayServices(tx, idx, conf, entMeta)
|
err = s.updateGatewayServices(tx, idx, conf, entMeta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to associate services to gateway: %v", err)
|
return fmt.Errorf("failed to associate services to gateway: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -282,14 +283,14 @@ func (s *Store) DeleteConfigEntry(idx uint64, kind, name string, entMeta *struct
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the config entry is for terminating gateways we delete entries from the memdb table
|
// If the config entry is for terminating or ingress gateways we delete entries from the memdb table
|
||||||
// that associates gateways <-> services.
|
// that associates gateways <-> services.
|
||||||
if kind == structs.TerminatingGateway {
|
if kind == structs.TerminatingGateway || kind == structs.IngressGateway {
|
||||||
if _, err := tx.DeleteAll(terminatingGatewayServicesTableName, "gateway", structs.NewServiceID(name, entMeta)); err != nil {
|
if _, err := tx.DeleteAll(gatewayServicesTableName, "gateway", structs.NewServiceID(name, entMeta)); err != nil {
|
||||||
return fmt.Errorf("failed to truncate gateway services table: %v", err)
|
return fmt.Errorf("failed to truncate gateway services table: %v", err)
|
||||||
}
|
}
|
||||||
if err := indexUpdateMaxTxn(tx, idx, terminatingGatewayServicesTableName); err != nil {
|
if err := indexUpdateMaxTxn(tx, idx, gatewayServicesTableName); err != nil {
|
||||||
return fmt.Errorf("failed updating terminating-gateway-services index: %v", err)
|
return fmt.Errorf("failed updating gateway-services index: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,7 +346,15 @@ func (s *Store) validateProposedConfigEntryInGraph(
|
||||||
case structs.ServiceSplitter:
|
case structs.ServiceSplitter:
|
||||||
case structs.ServiceResolver:
|
case structs.ServiceResolver:
|
||||||
case structs.IngressGateway:
|
case structs.IngressGateway:
|
||||||
|
err := s.checkGatewayClash(tx, name, structs.IngressGateway, structs.TerminatingGateway, entMeta)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
case structs.TerminatingGateway:
|
case structs.TerminatingGateway:
|
||||||
|
err := s.checkGatewayClash(tx, name, structs.TerminatingGateway, structs.IngressGateway, entMeta)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unhandled kind %q during validation of %q", kind, name)
|
return fmt.Errorf("unhandled kind %q during validation of %q", kind, name)
|
||||||
}
|
}
|
||||||
|
@ -353,6 +362,22 @@ func (s *Store) validateProposedConfigEntryInGraph(
|
||||||
return s.validateProposedConfigEntryInServiceGraph(tx, idx, kind, name, next, validateAllChains, entMeta)
|
return s.validateProposedConfigEntryInServiceGraph(tx, idx, kind, name, next, validateAllChains, entMeta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) checkGatewayClash(
|
||||||
|
tx *memdb.Txn,
|
||||||
|
name, selfKind, otherKind string,
|
||||||
|
entMeta *structs.EnterpriseMeta,
|
||||||
|
) error {
|
||||||
|
_, entry, err := s.configEntryTxn(tx, nil, otherKind, name, entMeta)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if entry != nil {
|
||||||
|
return fmt.Errorf("cannot create a %q config entry with name %q, "+
|
||||||
|
"a %q config entry with that name already exists", selfKind, name, otherKind)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var serviceGraphKinds = []string{
|
var serviceGraphKinds = []string{
|
||||||
structs.ServiceRouter,
|
structs.ServiceRouter,
|
||||||
structs.ServiceSplitter,
|
structs.ServiceSplitter,
|
||||||
|
|
|
@ -1250,3 +1250,37 @@ func TestStore_ReadDiscoveryChainConfigEntries_SubsetSplit(t *testing.T) {
|
||||||
require.Len(t, entrySet.Resolvers, 1)
|
require.Len(t, entrySet.Resolvers, 1)
|
||||||
require.Len(t, entrySet.Services, 1)
|
require.Len(t, entrySet.Services, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(ingress): test that having the same name in different namespace is valid
|
||||||
|
func TestStore_ValidateGatewayNamesCannotBeShared(t *testing.T) {
|
||||||
|
s := testStateStore(t)
|
||||||
|
|
||||||
|
ingress := &structs.IngressGatewayConfigEntry{
|
||||||
|
Kind: structs.IngressGateway,
|
||||||
|
Name: "gateway",
|
||||||
|
}
|
||||||
|
require.NoError(t, s.EnsureConfigEntry(0, ingress, nil))
|
||||||
|
|
||||||
|
terminating := &structs.TerminatingGatewayConfigEntry{
|
||||||
|
Kind: structs.TerminatingGateway,
|
||||||
|
Name: "gateway",
|
||||||
|
}
|
||||||
|
// Cannot have 2 gateways with same service name
|
||||||
|
require.Error(t, s.EnsureConfigEntry(1, terminating, nil))
|
||||||
|
|
||||||
|
ingress = &structs.IngressGatewayConfigEntry{
|
||||||
|
Kind: structs.IngressGateway,
|
||||||
|
Name: "gateway",
|
||||||
|
Listeners: []structs.IngressListener{
|
||||||
|
{Port: 8080},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.NoError(t, s.EnsureConfigEntry(2, ingress, nil))
|
||||||
|
require.NoError(t, s.DeleteConfigEntry(3, structs.IngressGateway, "gateway", nil))
|
||||||
|
|
||||||
|
// Adding the terminating gateway with same name should now work
|
||||||
|
require.NoError(t, s.EnsureConfigEntry(4, terminating, nil))
|
||||||
|
|
||||||
|
// Cannot have 2 gateways with same service name
|
||||||
|
require.Error(t, s.EnsureConfigEntry(5, ingress, nil))
|
||||||
|
}
|
||||||
|
|
|
@ -126,6 +126,31 @@ func testRegisterService(t *testing.T, s *Store, idx uint64, nodeID, serviceID s
|
||||||
testRegisterServiceWithChange(t, s, idx, nodeID, serviceID, false)
|
testRegisterServiceWithChange(t, s, idx, nodeID, serviceID, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testRegisterIngressService(t *testing.T, s *Store, idx uint64, nodeID, serviceID string) {
|
||||||
|
svc := &structs.NodeService{
|
||||||
|
ID: serviceID,
|
||||||
|
Service: serviceID,
|
||||||
|
Kind: structs.ServiceKindIngressGateway,
|
||||||
|
Address: "1.1.1.1",
|
||||||
|
Port: 1111,
|
||||||
|
}
|
||||||
|
if err := s.EnsureService(idx, nodeID, svc); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := s.db.Txn(false)
|
||||||
|
defer tx.Abort()
|
||||||
|
_, service, err := firstWatchCompoundWithTxn(tx, "services", "id", nil, nodeID, serviceID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
if result, ok := service.(*structs.ServiceNode); !ok ||
|
||||||
|
result.Node != nodeID ||
|
||||||
|
result.ServiceID != serviceID {
|
||||||
|
t.Fatalf("bad service: %#v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testRegisterCheck(t *testing.T, s *Store, idx uint64,
|
func testRegisterCheck(t *testing.T, s *Store, idx uint64,
|
||||||
nodeID string, serviceID string, checkID types.CheckID, state string) {
|
nodeID string, serviceID string, checkID types.CheckID, state string) {
|
||||||
chk := &structs.HealthCheck{
|
chk := &structs.HealthCheck{
|
||||||
|
|
101
agent/dns.go
101
agent/dns.go
|
@ -77,6 +77,17 @@ type dnsConfig struct {
|
||||||
enterpriseDNSConfig
|
enterpriseDNSConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type serviceLookup struct {
|
||||||
|
Network string
|
||||||
|
Datacenter string
|
||||||
|
Service string
|
||||||
|
Tag string
|
||||||
|
MaxRecursionLevel int
|
||||||
|
Connect bool
|
||||||
|
Ingress bool
|
||||||
|
structs.EnterpriseMeta
|
||||||
|
}
|
||||||
|
|
||||||
// DNSServer is used to wrap an Agent and expose various
|
// DNSServer is used to wrap an Agent and expose various
|
||||||
// service discovery endpoints using a DNS interface.
|
// service discovery endpoints using a DNS interface.
|
||||||
type DNSServer struct {
|
type DNSServer struct {
|
||||||
|
@ -501,7 +512,13 @@ func (d *DNSServer) addSOA(cfg *dnsConfig, msg *dns.Msg) {
|
||||||
// in the current cluster which serve as authoritative name servers for zone.
|
// in the current cluster which serve as authoritative name servers for zone.
|
||||||
|
|
||||||
func (d *DNSServer) nameservers(cfg *dnsConfig, maxRecursionLevel int) (ns []dns.RR, extra []dns.RR) {
|
func (d *DNSServer) nameservers(cfg *dnsConfig, maxRecursionLevel int) (ns []dns.RR, extra []dns.RR) {
|
||||||
out, err := d.lookupServiceNodes(cfg, d.agent.config.Datacenter, structs.ConsulServiceName, "", structs.DefaultEnterpriseMeta(), false)
|
out, err := d.lookupServiceNodes(cfg, serviceLookup{
|
||||||
|
Datacenter: d.agent.config.Datacenter,
|
||||||
|
Service: structs.ConsulServiceName,
|
||||||
|
Connect: false,
|
||||||
|
Ingress: false,
|
||||||
|
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.logger.Warn("Unable to get list of servers", "error", err)
|
d.logger.Warn("Unable to get list of servers", "error", err)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -598,7 +615,7 @@ func (d *DNSServer) doDispatch(network string, remoteAddr net.Addr, req, resp *d
|
||||||
done := false
|
done := false
|
||||||
for i := len(labels) - 1; i >= 0 && !done; i-- {
|
for i := len(labels) - 1; i >= 0 && !done; i-- {
|
||||||
switch labels[i] {
|
switch labels[i] {
|
||||||
case "service", "connect", "node", "query", "addr":
|
case "service", "connect", "ingress", "node", "query", "addr":
|
||||||
queryParts = labels[:i]
|
queryParts = labels[:i]
|
||||||
querySuffixes = labels[i+1:]
|
querySuffixes = labels[i+1:]
|
||||||
queryKind = labels[i]
|
queryKind = labels[i]
|
||||||
|
@ -630,6 +647,14 @@ func (d *DNSServer) doDispatch(network string, remoteAddr net.Addr, req, resp *d
|
||||||
goto INVALID
|
goto INVALID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lookup := serviceLookup{
|
||||||
|
Network: network,
|
||||||
|
Datacenter: datacenter,
|
||||||
|
Connect: false,
|
||||||
|
Ingress: false,
|
||||||
|
MaxRecursionLevel: maxRecursionLevel,
|
||||||
|
EnterpriseMeta: entMeta,
|
||||||
|
}
|
||||||
// Support RFC 2782 style syntax
|
// Support RFC 2782 style syntax
|
||||||
if n == 2 && strings.HasPrefix(queryParts[1], "_") && strings.HasPrefix(queryParts[0], "_") {
|
if n == 2 && strings.HasPrefix(queryParts[1], "_") && strings.HasPrefix(queryParts[0], "_") {
|
||||||
|
|
||||||
|
@ -641,8 +666,10 @@ func (d *DNSServer) doDispatch(network string, remoteAddr net.Addr, req, resp *d
|
||||||
tag = ""
|
tag = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lookup.Tag = tag
|
||||||
|
lookup.Service = queryParts[0][1:]
|
||||||
// _name._tag.service.consul
|
// _name._tag.service.consul
|
||||||
d.serviceLookup(cfg, network, datacenter, queryParts[0][1:], tag, &entMeta, false, req, resp, maxRecursionLevel)
|
d.serviceLookup(cfg, lookup, req, resp)
|
||||||
|
|
||||||
// Consul 0.3 and prior format for SRV queries
|
// Consul 0.3 and prior format for SRV queries
|
||||||
} else {
|
} else {
|
||||||
|
@ -653,8 +680,11 @@ func (d *DNSServer) doDispatch(network string, remoteAddr net.Addr, req, resp *d
|
||||||
tag = strings.Join(queryParts[:n-1], ".")
|
tag = strings.Join(queryParts[:n-1], ".")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lookup.Tag = tag
|
||||||
|
lookup.Service = queryParts[n-1]
|
||||||
|
|
||||||
// tag[.tag].name.service.consul
|
// tag[.tag].name.service.consul
|
||||||
d.serviceLookup(cfg, network, datacenter, queryParts[n-1], tag, &entMeta, false, req, resp, maxRecursionLevel)
|
d.serviceLookup(cfg, lookup, req, resp)
|
||||||
}
|
}
|
||||||
case "connect":
|
case "connect":
|
||||||
if len(queryParts) < 1 {
|
if len(queryParts) < 1 {
|
||||||
|
@ -665,8 +695,37 @@ func (d *DNSServer) doDispatch(network string, remoteAddr net.Addr, req, resp *d
|
||||||
goto INVALID
|
goto INVALID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lookup := serviceLookup{
|
||||||
|
Network: network,
|
||||||
|
Datacenter: datacenter,
|
||||||
|
Service: queryParts[len(queryParts)-1],
|
||||||
|
Connect: true,
|
||||||
|
Ingress: false,
|
||||||
|
MaxRecursionLevel: maxRecursionLevel,
|
||||||
|
EnterpriseMeta: entMeta,
|
||||||
|
}
|
||||||
// name.connect.consul
|
// name.connect.consul
|
||||||
d.serviceLookup(cfg, network, datacenter, queryParts[len(queryParts)-1], "", &entMeta, true, req, resp, maxRecursionLevel)
|
d.serviceLookup(cfg, lookup, req, resp)
|
||||||
|
case "ingress":
|
||||||
|
if len(queryParts) < 1 {
|
||||||
|
goto INVALID
|
||||||
|
}
|
||||||
|
|
||||||
|
if !d.parseDatacenterAndEnterpriseMeta(querySuffixes, cfg, &datacenter, &entMeta) {
|
||||||
|
goto INVALID
|
||||||
|
}
|
||||||
|
|
||||||
|
lookup := serviceLookup{
|
||||||
|
Network: network,
|
||||||
|
Datacenter: datacenter,
|
||||||
|
Service: queryParts[len(queryParts)-1],
|
||||||
|
Connect: false,
|
||||||
|
Ingress: true,
|
||||||
|
MaxRecursionLevel: maxRecursionLevel,
|
||||||
|
EnterpriseMeta: entMeta,
|
||||||
|
}
|
||||||
|
// name.ingress.consul
|
||||||
|
d.serviceLookup(cfg, lookup, req, resp)
|
||||||
case "node":
|
case "node":
|
||||||
if len(queryParts) < 1 {
|
if len(queryParts) < 1 {
|
||||||
goto INVALID
|
goto INVALID
|
||||||
|
@ -1076,22 +1135,20 @@ func (d *DNSServer) trimDNSResponse(cfg *dnsConfig, network string, req, resp *d
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookupServiceNodes returns nodes with a given service.
|
// lookupServiceNodes returns nodes with a given service.
|
||||||
func (d *DNSServer) lookupServiceNodes(cfg *dnsConfig, datacenter, service, tag string, entMeta *structs.EnterpriseMeta, connect bool) (structs.IndexedCheckServiceNodes, error) {
|
func (d *DNSServer) lookupServiceNodes(cfg *dnsConfig, lookup serviceLookup) (structs.IndexedCheckServiceNodes, error) {
|
||||||
args := structs.ServiceSpecificRequest{
|
args := structs.ServiceSpecificRequest{
|
||||||
Connect: connect,
|
Connect: lookup.Connect,
|
||||||
Datacenter: datacenter,
|
Ingress: lookup.Ingress,
|
||||||
ServiceName: service,
|
Datacenter: lookup.Datacenter,
|
||||||
ServiceTags: []string{tag},
|
ServiceName: lookup.Service,
|
||||||
TagFilter: tag != "",
|
ServiceTags: []string{lookup.Tag},
|
||||||
|
TagFilter: lookup.Tag != "",
|
||||||
QueryOptions: structs.QueryOptions{
|
QueryOptions: structs.QueryOptions{
|
||||||
Token: d.agent.tokens.UserToken(),
|
Token: d.agent.tokens.UserToken(),
|
||||||
AllowStale: cfg.AllowStale,
|
AllowStale: cfg.AllowStale,
|
||||||
MaxAge: cfg.CacheMaxAge,
|
MaxAge: cfg.CacheMaxAge,
|
||||||
},
|
},
|
||||||
}
|
EnterpriseMeta: lookup.EnterpriseMeta,
|
||||||
|
|
||||||
if entMeta != nil {
|
|
||||||
args.EnterpriseMeta = *entMeta
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var out structs.IndexedCheckServiceNodes
|
var out structs.IndexedCheckServiceNodes
|
||||||
|
@ -1108,7 +1165,7 @@ func (d *DNSServer) lookupServiceNodes(cfg *dnsConfig, datacenter, service, tag
|
||||||
}
|
}
|
||||||
d.logger.Trace("cache results for service",
|
d.logger.Trace("cache results for service",
|
||||||
"cache_hit", m.Hit,
|
"cache_hit", m.Hit,
|
||||||
"service", service,
|
"service", lookup.Service,
|
||||||
)
|
)
|
||||||
|
|
||||||
out = *reply
|
out = *reply
|
||||||
|
@ -1141,8 +1198,8 @@ func (d *DNSServer) lookupServiceNodes(cfg *dnsConfig, datacenter, service, tag
|
||||||
}
|
}
|
||||||
|
|
||||||
// serviceLookup is used to handle a service query
|
// serviceLookup is used to handle a service query
|
||||||
func (d *DNSServer) serviceLookup(cfg *dnsConfig, network, datacenter, service, tag string, entMeta *structs.EnterpriseMeta, connect bool, req, resp *dns.Msg, maxRecursionLevel int) {
|
func (d *DNSServer) serviceLookup(cfg *dnsConfig, lookup serviceLookup, req, resp *dns.Msg) {
|
||||||
out, err := d.lookupServiceNodes(cfg, datacenter, service, tag, entMeta, connect)
|
out, err := d.lookupServiceNodes(cfg, lookup)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.logger.Error("rpc error", "error", err)
|
d.logger.Error("rpc error", "error", err)
|
||||||
resp.SetRcode(req, dns.RcodeServerFailure)
|
resp.SetRcode(req, dns.RcodeServerFailure)
|
||||||
|
@ -1160,17 +1217,17 @@ func (d *DNSServer) serviceLookup(cfg *dnsConfig, network, datacenter, service,
|
||||||
out.Nodes.Shuffle()
|
out.Nodes.Shuffle()
|
||||||
|
|
||||||
// Determine the TTL
|
// Determine the TTL
|
||||||
ttl, _ := cfg.GetTTLForService(service)
|
ttl, _ := cfg.GetTTLForService(lookup.Service)
|
||||||
|
|
||||||
// Add various responses depending on the request
|
// Add various responses depending on the request
|
||||||
qType := req.Question[0].Qtype
|
qType := req.Question[0].Qtype
|
||||||
if qType == dns.TypeSRV {
|
if qType == dns.TypeSRV {
|
||||||
d.serviceSRVRecords(cfg, datacenter, out.Nodes, req, resp, ttl, maxRecursionLevel)
|
d.serviceSRVRecords(cfg, lookup.Datacenter, out.Nodes, req, resp, ttl, lookup.MaxRecursionLevel)
|
||||||
} else {
|
} else {
|
||||||
d.serviceNodeRecords(cfg, datacenter, out.Nodes, req, resp, ttl, maxRecursionLevel)
|
d.serviceNodeRecords(cfg, lookup.Datacenter, out.Nodes, req, resp, ttl, lookup.MaxRecursionLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
d.trimDNSResponse(cfg, network, req, resp)
|
d.trimDNSResponse(cfg, lookup.Network, req, resp)
|
||||||
|
|
||||||
// If the answer is empty and the response isn't truncated, return not found
|
// If the answer is empty and the response isn't truncated, return not found
|
||||||
if len(resp.Answer) == 0 && !resp.Truncated {
|
if len(resp.Answer) == 0 && !resp.Truncated {
|
||||||
|
|
|
@ -1636,6 +1636,90 @@ func TestDNS_ConnectServiceLookup(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDNS_IngressServiceLookup(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
a := NewTestAgent(t, "")
|
||||||
|
defer a.Shutdown()
|
||||||
|
testrpc.WaitForLeader(t, a.RPC, "dc1")
|
||||||
|
|
||||||
|
// Register ingress-gateway service
|
||||||
|
{
|
||||||
|
args := structs.TestRegisterIngressGateway(t)
|
||||||
|
var out struct{}
|
||||||
|
require.Nil(t, a.RPC("Catalog.Register", args, &out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register db service
|
||||||
|
{
|
||||||
|
args := &structs.RegisterRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foo",
|
||||||
|
Address: "127.0.0.1",
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
Service: "db",
|
||||||
|
Address: "",
|
||||||
|
Port: 80,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var out struct{}
|
||||||
|
require.Nil(t, a.RPC("Catalog.Register", args, &out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register ingress-gateway config entry
|
||||||
|
{
|
||||||
|
args := &structs.IngressGatewayConfigEntry{
|
||||||
|
Name: "ingress-gateway",
|
||||||
|
Kind: structs.IngressGateway,
|
||||||
|
Listeners: []structs.IngressListener{
|
||||||
|
{
|
||||||
|
Port: 8888,
|
||||||
|
Protocol: "http",
|
||||||
|
Services: []structs.IngressService{
|
||||||
|
{Name: "db"},
|
||||||
|
{Name: "api"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := structs.ConfigEntryRequest{
|
||||||
|
Op: structs.ConfigEntryUpsert,
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Entry: args,
|
||||||
|
}
|
||||||
|
var out bool
|
||||||
|
require.Nil(t, a.RPC("ConfigEntry.Apply", req, &out))
|
||||||
|
require.True(t, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up the service
|
||||||
|
questions := []string{
|
||||||
|
"api.ingress.consul.",
|
||||||
|
"api.ingress.dc1.consul.",
|
||||||
|
"db.ingress.consul.",
|
||||||
|
"db.ingress.dc1.consul.",
|
||||||
|
}
|
||||||
|
for _, question := range questions {
|
||||||
|
t.Run(question, func(t *testing.T) {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(question, dns.TypeA)
|
||||||
|
|
||||||
|
c := new(dns.Client)
|
||||||
|
in, _, err := c.Exchange(m, a.DNSAddr())
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Len(t, in.Answer, 1)
|
||||||
|
|
||||||
|
cnameRec, ok := in.Answer[0].(*dns.A)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, question, cnameRec.Hdr.Name)
|
||||||
|
require.Equal(t, uint32(0), cnameRec.Hdr.Ttl)
|
||||||
|
require.Equal(t, "127.0.0.1", cnameRec.A.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDNS_ExternalServiceLookup(t *testing.T) {
|
func TestDNS_ExternalServiceLookup(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
a := NewTestAgent(t, "")
|
a := NewTestAgent(t, "")
|
||||||
|
|
|
@ -133,7 +133,9 @@ func (m *Manager) syncState() {
|
||||||
// Traverse the local state and ensure all proxy services are registered
|
// Traverse the local state and ensure all proxy services are registered
|
||||||
services := m.State.Services(structs.WildcardEnterpriseMeta())
|
services := m.State.Services(structs.WildcardEnterpriseMeta())
|
||||||
for sid, svc := range services {
|
for sid, svc := range services {
|
||||||
if svc.Kind != structs.ServiceKindConnectProxy && svc.Kind != structs.ServiceKindMeshGateway {
|
if svc.Kind != structs.ServiceKindConnectProxy &&
|
||||||
|
svc.Kind != structs.ServiceKindMeshGateway &&
|
||||||
|
svc.Kind != structs.ServiceKindIngressGateway {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// TODO(banks): need to work out when to default some stuff. For example
|
// TODO(banks): need to work out when to default some stuff. For example
|
||||||
|
|
|
@ -202,19 +202,21 @@ func TestManager_BasicLifecycle(t *testing.T) {
|
||||||
TaggedAddresses: make(map[string]structs.ServiceAddress),
|
TaggedAddresses: make(map[string]structs.ServiceAddress),
|
||||||
Roots: roots,
|
Roots: roots,
|
||||||
ConnectProxy: configSnapshotConnectProxy{
|
ConnectProxy: configSnapshotConnectProxy{
|
||||||
Leaf: leaf,
|
ConfigSnapshotUpstreams: ConfigSnapshotUpstreams{
|
||||||
DiscoveryChain: map[string]*structs.CompiledDiscoveryChain{
|
Leaf: leaf,
|
||||||
"db": dbDefaultChain(),
|
DiscoveryChain: map[string]*structs.CompiledDiscoveryChain{
|
||||||
},
|
"db": dbDefaultChain(),
|
||||||
WatchedUpstreams: nil, // Clone() clears this out
|
},
|
||||||
WatchedUpstreamEndpoints: map[string]map[string]structs.CheckServiceNodes{
|
WatchedUpstreams: nil, // Clone() clears this out
|
||||||
"db": {
|
WatchedUpstreamEndpoints: map[string]map[string]structs.CheckServiceNodes{
|
||||||
"db.default.dc1": TestUpstreamNodes(t),
|
"db": {
|
||||||
|
"db.default.dc1": TestUpstreamNodes(t),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
WatchedGateways: nil, // Clone() clears this out
|
||||||
|
WatchedGatewayEndpoints: map[string]map[string]structs.CheckServiceNodes{
|
||||||
|
"db": {},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
WatchedGateways: nil, // Clone() clears this out
|
|
||||||
WatchedGatewayEndpoints: map[string]map[string]structs.CheckServiceNodes{
|
|
||||||
"db": {},
|
|
||||||
},
|
},
|
||||||
PreparedQueryEndpoints: map[string]structs.CheckServiceNodes{},
|
PreparedQueryEndpoints: map[string]structs.CheckServiceNodes{},
|
||||||
WatchedServiceChecks: map[structs.ServiceID][]structs.CheckType{},
|
WatchedServiceChecks: map[structs.ServiceID][]structs.CheckType{},
|
||||||
|
@ -247,20 +249,22 @@ func TestManager_BasicLifecycle(t *testing.T) {
|
||||||
TaggedAddresses: make(map[string]structs.ServiceAddress),
|
TaggedAddresses: make(map[string]structs.ServiceAddress),
|
||||||
Roots: roots,
|
Roots: roots,
|
||||||
ConnectProxy: configSnapshotConnectProxy{
|
ConnectProxy: configSnapshotConnectProxy{
|
||||||
Leaf: leaf,
|
ConfigSnapshotUpstreams: ConfigSnapshotUpstreams{
|
||||||
DiscoveryChain: map[string]*structs.CompiledDiscoveryChain{
|
Leaf: leaf,
|
||||||
"db": dbSplitChain(),
|
DiscoveryChain: map[string]*structs.CompiledDiscoveryChain{
|
||||||
},
|
"db": dbSplitChain(),
|
||||||
WatchedUpstreams: nil, // Clone() clears this out
|
},
|
||||||
WatchedUpstreamEndpoints: map[string]map[string]structs.CheckServiceNodes{
|
WatchedUpstreams: nil, // Clone() clears this out
|
||||||
"db": {
|
WatchedUpstreamEndpoints: map[string]map[string]structs.CheckServiceNodes{
|
||||||
"v1.db.default.dc1": TestUpstreamNodes(t),
|
"db": {
|
||||||
"v2.db.default.dc1": TestUpstreamNodesAlternate(t),
|
"v1.db.default.dc1": TestUpstreamNodes(t),
|
||||||
|
"v2.db.default.dc1": TestUpstreamNodesAlternate(t),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
WatchedGateways: nil, // Clone() clears this out
|
||||||
|
WatchedGatewayEndpoints: map[string]map[string]structs.CheckServiceNodes{
|
||||||
|
"db": {},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
WatchedGateways: nil, // Clone() clears this out
|
|
||||||
WatchedGatewayEndpoints: map[string]map[string]structs.CheckServiceNodes{
|
|
||||||
"db": {},
|
|
||||||
},
|
},
|
||||||
PreparedQueryEndpoints: map[string]structs.CheckServiceNodes{},
|
PreparedQueryEndpoints: map[string]structs.CheckServiceNodes{},
|
||||||
WatchedServiceChecks: map[structs.ServiceID][]structs.CheckType{},
|
WatchedServiceChecks: map[structs.ServiceID][]structs.CheckType{},
|
||||||
|
|
|
@ -7,16 +7,40 @@ import (
|
||||||
"github.com/mitchellh/copystructure"
|
"github.com/mitchellh/copystructure"
|
||||||
)
|
)
|
||||||
|
|
||||||
type configSnapshotConnectProxy struct {
|
// TODO(ingress): Can we think of a better for this bag of data?
|
||||||
Leaf *structs.IssuedCert
|
// A shared data structure that contains information about discovered upstreams
|
||||||
DiscoveryChain map[string]*structs.CompiledDiscoveryChain // this is keyed by the Upstream.Identifier(), not the chain name
|
type ConfigSnapshotUpstreams struct {
|
||||||
WatchedUpstreams map[string]map[string]context.CancelFunc
|
Leaf *structs.IssuedCert
|
||||||
WatchedUpstreamEndpoints map[string]map[string]structs.CheckServiceNodes
|
// DiscoveryChain is a map of upstream.Identifier() ->
|
||||||
WatchedGateways map[string]map[string]context.CancelFunc
|
// CompiledDiscoveryChain's, and is used to determine what services could be
|
||||||
WatchedGatewayEndpoints map[string]map[string]structs.CheckServiceNodes
|
// targeted by this upstream. We then instantiate watches for those targets.
|
||||||
WatchedServiceChecks map[structs.ServiceID][]structs.CheckType // TODO: missing garbage collection
|
DiscoveryChain map[string]*structs.CompiledDiscoveryChain
|
||||||
|
|
||||||
PreparedQueryEndpoints map[string]structs.CheckServiceNodes // DEPRECATED:see:WatchedUpstreamEndpoints
|
// WatchedUpstreams is a map of upstream.Identifier() -> (map of TargetID ->
|
||||||
|
// CancelFunc's) in order to cancel any watches when the configuration is
|
||||||
|
// changed.
|
||||||
|
WatchedUpstreams map[string]map[string]context.CancelFunc
|
||||||
|
|
||||||
|
// WatchedUpstreamEndpoints is a map of upstream.Identifier() -> (map of
|
||||||
|
// TargetID -> CheckServiceNodes) and is used to determine the backing
|
||||||
|
// endpoints of an upstream.
|
||||||
|
WatchedUpstreamEndpoints map[string]map[string]structs.CheckServiceNodes
|
||||||
|
|
||||||
|
// WatchedGateways is a map of upstream.Identifier() -> (map of
|
||||||
|
// TargetID -> CancelFunc) in order to cancel watches for mesh gateways
|
||||||
|
WatchedGateways map[string]map[string]context.CancelFunc
|
||||||
|
|
||||||
|
// WatchedGatewayEndpoints is a map of upstream.Identifier() -> (map of
|
||||||
|
// TargetID -> CheckServiceNodes) and is used to determine the backing
|
||||||
|
// endpoints of a mesh gateway.
|
||||||
|
WatchedGatewayEndpoints map[string]map[string]structs.CheckServiceNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
type configSnapshotConnectProxy struct {
|
||||||
|
ConfigSnapshotUpstreams
|
||||||
|
|
||||||
|
WatchedServiceChecks map[structs.ServiceID][]structs.CheckType // TODO: missing garbage collection
|
||||||
|
PreparedQueryEndpoints map[string]structs.CheckServiceNodes // DEPRECATED:see:WatchedUpstreamEndpoints
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *configSnapshotConnectProxy) IsEmpty() bool {
|
func (c *configSnapshotConnectProxy) IsEmpty() bool {
|
||||||
|
@ -108,6 +132,31 @@ func (c *configSnapshotMeshGateway) IsEmpty() bool {
|
||||||
len(c.ConsulServers) == 0
|
len(c.ConsulServers) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type configSnapshotIngressGateway struct {
|
||||||
|
ConfigSnapshotUpstreams
|
||||||
|
// Upstreams is a list of upstreams this ingress gateway should serve traffic
|
||||||
|
// to. This is constructed from the ingress-gateway config entry, and uses
|
||||||
|
// the GatewayServices RPC to retrieve them.
|
||||||
|
Upstreams []structs.Upstream
|
||||||
|
|
||||||
|
// WatchedDiscoveryChains is a map of upstream.Identifier() -> CancelFunc's
|
||||||
|
// in order to cancel any watches when the ingress gateway configuration is
|
||||||
|
// changed. Ingress gateways need this because discovery chain watches are
|
||||||
|
// added and removed through the lifecycle of single proxycfg.state instance.
|
||||||
|
WatchedDiscoveryChains map[string]context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configSnapshotIngressGateway) IsEmpty() bool {
|
||||||
|
if c == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return len(c.Upstreams) == 0 &&
|
||||||
|
len(c.DiscoveryChain) == 0 &&
|
||||||
|
len(c.WatchedDiscoveryChains) == 0 &&
|
||||||
|
len(c.WatchedUpstreams) == 0 &&
|
||||||
|
len(c.WatchedUpstreamEndpoints) == 0
|
||||||
|
}
|
||||||
|
|
||||||
// ConfigSnapshot captures all the resulting config needed for a proxy instance.
|
// ConfigSnapshot captures all the resulting config needed for a proxy instance.
|
||||||
// It is meant to be point-in-time coherent and is used to deliver the current
|
// It is meant to be point-in-time coherent and is used to deliver the current
|
||||||
// config state to observers who need it to be pushed in (e.g. XDS server).
|
// config state to observers who need it to be pushed in (e.g. XDS server).
|
||||||
|
@ -131,6 +180,9 @@ type ConfigSnapshot struct {
|
||||||
// mesh-gateway specific
|
// mesh-gateway specific
|
||||||
MeshGateway configSnapshotMeshGateway
|
MeshGateway configSnapshotMeshGateway
|
||||||
|
|
||||||
|
// ingress-gateway specific
|
||||||
|
IngressGateway configSnapshotIngressGateway
|
||||||
|
|
||||||
// Skip intentions for now as we don't push those down yet, just pre-warm them.
|
// Skip intentions for now as we don't push those down yet, just pre-warm them.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,6 +198,9 @@ func (s *ConfigSnapshot) Valid() bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return s.Roots != nil && (s.MeshGateway.WatchedServicesSet || len(s.MeshGateway.ServiceGroups) > 0)
|
return s.Roots != nil && (s.MeshGateway.WatchedServicesSet || len(s.MeshGateway.ServiceGroups) > 0)
|
||||||
|
case structs.ServiceKindIngressGateway:
|
||||||
|
return s.Roots != nil &&
|
||||||
|
s.IngressGateway.Leaf != nil
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -169,7 +224,21 @@ func (s *ConfigSnapshot) Clone() (*ConfigSnapshot, error) {
|
||||||
case structs.ServiceKindMeshGateway:
|
case structs.ServiceKindMeshGateway:
|
||||||
snap.MeshGateway.WatchedDatacenters = nil
|
snap.MeshGateway.WatchedDatacenters = nil
|
||||||
snap.MeshGateway.WatchedServices = nil
|
snap.MeshGateway.WatchedServices = nil
|
||||||
|
case structs.ServiceKindIngressGateway:
|
||||||
|
snap.IngressGateway.WatchedUpstreams = nil
|
||||||
|
snap.IngressGateway.WatchedDiscoveryChains = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return snap, nil
|
return snap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ConfigSnapshot) Leaf() *structs.IssuedCert {
|
||||||
|
switch s.Kind {
|
||||||
|
case structs.ServiceKindConnectProxy:
|
||||||
|
return s.ConnectProxy.Leaf
|
||||||
|
case structs.ServiceKindIngressGateway:
|
||||||
|
return s.IngressGateway.Leaf
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ const (
|
||||||
consulServerListWatchID = "consul-server-list"
|
consulServerListWatchID = "consul-server-list"
|
||||||
datacentersWatchID = "datacenters"
|
datacentersWatchID = "datacenters"
|
||||||
serviceResolversWatchID = "service-resolvers"
|
serviceResolversWatchID = "service-resolvers"
|
||||||
|
gatewayServicesWatchID = "gateway-services"
|
||||||
svcChecksWatchIDPrefix = cachetype.ServiceHTTPChecksName + ":"
|
svcChecksWatchIDPrefix = cachetype.ServiceHTTPChecksName + ":"
|
||||||
serviceIDPrefix = string(structs.UpstreamDestTypeService) + ":"
|
serviceIDPrefix = string(structs.UpstreamDestTypeService) + ":"
|
||||||
preparedQueryIDPrefix = string(structs.UpstreamDestTypePreparedQuery) + ":"
|
preparedQueryIDPrefix = string(structs.UpstreamDestTypePreparedQuery) + ":"
|
||||||
|
@ -106,8 +107,12 @@ func copyProxyConfig(ns *structs.NodeService) (structs.ConnectProxyConfig, error
|
||||||
// The returned state needs its required dependencies to be set before Watch
|
// The returned state needs its required dependencies to be set before Watch
|
||||||
// can be called.
|
// can be called.
|
||||||
func newState(ns *structs.NodeService, token string) (*state, error) {
|
func newState(ns *structs.NodeService, token string) (*state, error) {
|
||||||
if ns.Kind != structs.ServiceKindConnectProxy && ns.Kind != structs.ServiceKindMeshGateway {
|
switch ns.Kind {
|
||||||
return nil, errors.New("not a connect-proxy or mesh-gateway")
|
case structs.ServiceKindConnectProxy:
|
||||||
|
case structs.ServiceKindMeshGateway:
|
||||||
|
case structs.ServiceKindIngressGateway:
|
||||||
|
default:
|
||||||
|
return nil, errors.New("not a connect-proxy, mesh-gateway, or ingress-gateway")
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyCfg, err := copyProxyConfig(ns)
|
proxyCfg, err := copyProxyConfig(ns)
|
||||||
|
@ -181,6 +186,8 @@ func (s *state) initWatches() error {
|
||||||
return s.initWatchesConnectProxy()
|
return s.initWatchesConnectProxy()
|
||||||
case structs.ServiceKindMeshGateway:
|
case structs.ServiceKindMeshGateway:
|
||||||
return s.initWatchesMeshGateway()
|
return s.initWatchesMeshGateway()
|
||||||
|
case structs.ServiceKindIngressGateway:
|
||||||
|
return s.initWatchesIngressGateway()
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("Unsupported service kind")
|
return fmt.Errorf("Unsupported service kind")
|
||||||
}
|
}
|
||||||
|
@ -432,6 +439,42 @@ func (s *state) initWatchesMeshGateway() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *state) initWatchesIngressGateway() error {
|
||||||
|
// Watch for root changes
|
||||||
|
err := s.cache.Notify(s.ctx, cachetype.ConnectCARootName, &structs.DCSpecificRequest{
|
||||||
|
Datacenter: s.source.Datacenter,
|
||||||
|
QueryOptions: structs.QueryOptions{Token: s.token},
|
||||||
|
Source: *s.source,
|
||||||
|
}, rootsWatchID, s.ch)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch the leaf cert
|
||||||
|
err = s.cache.Notify(s.ctx, cachetype.ConnectCALeafName, &cachetype.ConnectCALeafRequest{
|
||||||
|
Datacenter: s.source.Datacenter,
|
||||||
|
Token: s.token,
|
||||||
|
Service: s.service,
|
||||||
|
EnterpriseMeta: s.proxyID.EnterpriseMeta,
|
||||||
|
}, leafWatchID, s.ch)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch the ingress-gateway's list of upstreams
|
||||||
|
err = s.cache.Notify(s.ctx, cachetype.GatewayServicesName, &structs.ServiceSpecificRequest{
|
||||||
|
Datacenter: s.source.Datacenter,
|
||||||
|
QueryOptions: structs.QueryOptions{Token: s.token},
|
||||||
|
ServiceName: s.service,
|
||||||
|
EnterpriseMeta: s.proxyID.EnterpriseMeta,
|
||||||
|
}, gatewayServicesWatchID, s.ch)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *state) initialConfigSnapshot() ConfigSnapshot {
|
func (s *state) initialConfigSnapshot() ConfigSnapshot {
|
||||||
snap := ConfigSnapshot{
|
snap := ConfigSnapshot{
|
||||||
Kind: s.kind,
|
Kind: s.kind,
|
||||||
|
@ -464,6 +507,13 @@ func (s *state) initialConfigSnapshot() ConfigSnapshot {
|
||||||
snap.MeshGateway.ServiceResolvers = make(map[structs.ServiceID]*structs.ServiceResolverConfigEntry)
|
snap.MeshGateway.ServiceResolvers = make(map[structs.ServiceID]*structs.ServiceResolverConfigEntry)
|
||||||
// there is no need to initialize the map of service resolvers as we
|
// there is no need to initialize the map of service resolvers as we
|
||||||
// fully rebuild it every time we get updates
|
// fully rebuild it every time we get updates
|
||||||
|
case structs.ServiceKindIngressGateway:
|
||||||
|
snap.IngressGateway.WatchedDiscoveryChains = make(map[string]context.CancelFunc)
|
||||||
|
snap.IngressGateway.DiscoveryChain = make(map[string]*structs.CompiledDiscoveryChain)
|
||||||
|
snap.IngressGateway.WatchedUpstreams = make(map[string]map[string]context.CancelFunc)
|
||||||
|
snap.IngressGateway.WatchedUpstreamEndpoints = make(map[string]map[string]structs.CheckServiceNodes)
|
||||||
|
snap.IngressGateway.WatchedGateways = make(map[string]map[string]context.CancelFunc)
|
||||||
|
snap.IngressGateway.WatchedGatewayEndpoints = make(map[string]map[string]structs.CheckServiceNodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
return snap
|
return snap
|
||||||
|
@ -563,6 +613,8 @@ func (s *state) handleUpdate(u cache.UpdateEvent, snap *ConfigSnapshot) error {
|
||||||
return s.handleUpdateConnectProxy(u, snap)
|
return s.handleUpdateConnectProxy(u, snap)
|
||||||
case structs.ServiceKindMeshGateway:
|
case structs.ServiceKindMeshGateway:
|
||||||
return s.handleUpdateMeshGateway(u, snap)
|
return s.handleUpdateMeshGateway(u, snap)
|
||||||
|
case structs.ServiceKindIngressGateway:
|
||||||
|
return s.handleUpdateIngressGateway(u, snap)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("Unsupported service kind")
|
return fmt.Errorf("Unsupported service kind")
|
||||||
}
|
}
|
||||||
|
@ -580,64 +632,9 @@ func (s *state) handleUpdateConnectProxy(u cache.UpdateEvent, snap *ConfigSnapsh
|
||||||
return fmt.Errorf("invalid type for response: %T", u.Result)
|
return fmt.Errorf("invalid type for response: %T", u.Result)
|
||||||
}
|
}
|
||||||
snap.Roots = roots
|
snap.Roots = roots
|
||||||
|
|
||||||
case u.CorrelationID == leafWatchID:
|
|
||||||
leaf, ok := u.Result.(*structs.IssuedCert)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid type for response: %T", u.Result)
|
|
||||||
}
|
|
||||||
snap.ConnectProxy.Leaf = leaf
|
|
||||||
|
|
||||||
case u.CorrelationID == intentionsWatchID:
|
case u.CorrelationID == intentionsWatchID:
|
||||||
// Not in snapshot currently, no op
|
// Not in snapshot currently, no op
|
||||||
|
|
||||||
case strings.HasPrefix(u.CorrelationID, "discovery-chain:"):
|
|
||||||
resp, ok := u.Result.(*structs.DiscoveryChainResponse)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid type for response: %T", u.Result)
|
|
||||||
}
|
|
||||||
svc := strings.TrimPrefix(u.CorrelationID, "discovery-chain:")
|
|
||||||
snap.ConnectProxy.DiscoveryChain[svc] = resp.Chain
|
|
||||||
|
|
||||||
if err := s.resetWatchesFromChain(svc, resp.Chain, snap); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
case strings.HasPrefix(u.CorrelationID, "upstream-target:"):
|
|
||||||
resp, ok := u.Result.(*structs.IndexedCheckServiceNodes)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid type for response: %T", u.Result)
|
|
||||||
}
|
|
||||||
correlationID := strings.TrimPrefix(u.CorrelationID, "upstream-target:")
|
|
||||||
targetID, svc, ok := removeColonPrefix(correlationID)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid correlation id %q", u.CorrelationID)
|
|
||||||
}
|
|
||||||
|
|
||||||
m, ok := snap.ConnectProxy.WatchedUpstreamEndpoints[svc]
|
|
||||||
if !ok {
|
|
||||||
m = make(map[string]structs.CheckServiceNodes)
|
|
||||||
snap.ConnectProxy.WatchedUpstreamEndpoints[svc] = m
|
|
||||||
}
|
|
||||||
snap.ConnectProxy.WatchedUpstreamEndpoints[svc][targetID] = resp.Nodes
|
|
||||||
|
|
||||||
case strings.HasPrefix(u.CorrelationID, "mesh-gateway:"):
|
|
||||||
resp, ok := u.Result.(*structs.IndexedCheckServiceNodes)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid type for response: %T", u.Result)
|
|
||||||
}
|
|
||||||
correlationID := strings.TrimPrefix(u.CorrelationID, "mesh-gateway:")
|
|
||||||
dc, svc, ok := removeColonPrefix(correlationID)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid correlation id %q", u.CorrelationID)
|
|
||||||
}
|
|
||||||
m, ok := snap.ConnectProxy.WatchedGatewayEndpoints[svc]
|
|
||||||
if !ok {
|
|
||||||
m = make(map[string]structs.CheckServiceNodes)
|
|
||||||
snap.ConnectProxy.WatchedGatewayEndpoints[svc] = m
|
|
||||||
}
|
|
||||||
snap.ConnectProxy.WatchedGatewayEndpoints[svc][dc] = resp.Nodes
|
|
||||||
|
|
||||||
case strings.HasPrefix(u.CorrelationID, "upstream:"+preparedQueryIDPrefix):
|
case strings.HasPrefix(u.CorrelationID, "upstream:"+preparedQueryIDPrefix):
|
||||||
resp, ok := u.Result.(*structs.PreparedQueryExecuteResponse)
|
resp, ok := u.Result.(*structs.PreparedQueryExecuteResponse)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -653,7 +650,71 @@ func (s *state) handleUpdateConnectProxy(u cache.UpdateEvent, snap *ConfigSnapsh
|
||||||
}
|
}
|
||||||
svcID := structs.ServiceIDFromString(strings.TrimPrefix(u.CorrelationID, svcChecksWatchIDPrefix))
|
svcID := structs.ServiceIDFromString(strings.TrimPrefix(u.CorrelationID, svcChecksWatchIDPrefix))
|
||||||
snap.ConnectProxy.WatchedServiceChecks[svcID] = resp
|
snap.ConnectProxy.WatchedServiceChecks[svcID] = resp
|
||||||
|
default:
|
||||||
|
return s.handleUpdateUpstreams(u, &snap.ConnectProxy.ConfigSnapshotUpstreams)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) handleUpdateUpstreams(u cache.UpdateEvent, snap *ConfigSnapshotUpstreams) error {
|
||||||
|
if u.Err != nil {
|
||||||
|
return fmt.Errorf("error filling agent cache: %v", u.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case u.CorrelationID == leafWatchID:
|
||||||
|
leaf, ok := u.Result.(*structs.IssuedCert)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid type for response: %T", u.Result)
|
||||||
|
}
|
||||||
|
snap.Leaf = leaf
|
||||||
|
|
||||||
|
case strings.HasPrefix(u.CorrelationID, "discovery-chain:"):
|
||||||
|
resp, ok := u.Result.(*structs.DiscoveryChainResponse)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid type for response: %T", u.Result)
|
||||||
|
}
|
||||||
|
svc := strings.TrimPrefix(u.CorrelationID, "discovery-chain:")
|
||||||
|
snap.DiscoveryChain[svc] = resp.Chain
|
||||||
|
|
||||||
|
if err := s.resetWatchesFromChain(svc, resp.Chain, snap); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
case strings.HasPrefix(u.CorrelationID, "upstream-target:"):
|
||||||
|
resp, ok := u.Result.(*structs.IndexedCheckServiceNodes)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid type for response: %T", u.Result)
|
||||||
|
}
|
||||||
|
correlationID := strings.TrimPrefix(u.CorrelationID, "upstream-target:")
|
||||||
|
targetID, svc, ok := removeColonPrefix(correlationID)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid correlation id %q", u.CorrelationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
m, ok := snap.WatchedUpstreamEndpoints[svc]
|
||||||
|
if !ok {
|
||||||
|
m = make(map[string]structs.CheckServiceNodes)
|
||||||
|
snap.WatchedUpstreamEndpoints[svc] = m
|
||||||
|
}
|
||||||
|
snap.WatchedUpstreamEndpoints[svc][targetID] = resp.Nodes
|
||||||
|
|
||||||
|
case strings.HasPrefix(u.CorrelationID, "mesh-gateway:"):
|
||||||
|
resp, ok := u.Result.(*structs.IndexedCheckServiceNodes)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid type for response: %T", u.Result)
|
||||||
|
}
|
||||||
|
correlationID := strings.TrimPrefix(u.CorrelationID, "mesh-gateway:")
|
||||||
|
dc, svc, ok := removeColonPrefix(correlationID)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid correlation id %q", u.CorrelationID)
|
||||||
|
}
|
||||||
|
m, ok := snap.WatchedGatewayEndpoints[svc]
|
||||||
|
if !ok {
|
||||||
|
m = make(map[string]structs.CheckServiceNodes)
|
||||||
|
snap.WatchedGatewayEndpoints[svc] = m
|
||||||
|
}
|
||||||
|
snap.WatchedGatewayEndpoints[svc][dc] = resp.Nodes
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown correlation ID: %s", u.CorrelationID)
|
return fmt.Errorf("unknown correlation ID: %s", u.CorrelationID)
|
||||||
}
|
}
|
||||||
|
@ -671,7 +732,7 @@ func removeColonPrefix(s string) (string, string, bool) {
|
||||||
func (s *state) resetWatchesFromChain(
|
func (s *state) resetWatchesFromChain(
|
||||||
id string,
|
id string,
|
||||||
chain *structs.CompiledDiscoveryChain,
|
chain *structs.CompiledDiscoveryChain,
|
||||||
snap *ConfigSnapshot,
|
snap *ConfigSnapshotUpstreams,
|
||||||
) error {
|
) error {
|
||||||
s.logger.Trace("resetting watches for discovery chain", "id", id)
|
s.logger.Trace("resetting watches for discovery chain", "id", id)
|
||||||
if chain == nil {
|
if chain == nil {
|
||||||
|
@ -679,17 +740,17 @@ func (s *state) resetWatchesFromChain(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize relevant sub maps.
|
// Initialize relevant sub maps.
|
||||||
if _, ok := snap.ConnectProxy.WatchedUpstreams[id]; !ok {
|
if _, ok := snap.WatchedUpstreams[id]; !ok {
|
||||||
snap.ConnectProxy.WatchedUpstreams[id] = make(map[string]context.CancelFunc)
|
snap.WatchedUpstreams[id] = make(map[string]context.CancelFunc)
|
||||||
}
|
}
|
||||||
if _, ok := snap.ConnectProxy.WatchedUpstreamEndpoints[id]; !ok {
|
if _, ok := snap.WatchedUpstreamEndpoints[id]; !ok {
|
||||||
snap.ConnectProxy.WatchedUpstreamEndpoints[id] = make(map[string]structs.CheckServiceNodes)
|
snap.WatchedUpstreamEndpoints[id] = make(map[string]structs.CheckServiceNodes)
|
||||||
}
|
}
|
||||||
if _, ok := snap.ConnectProxy.WatchedGateways[id]; !ok {
|
if _, ok := snap.WatchedGateways[id]; !ok {
|
||||||
snap.ConnectProxy.WatchedGateways[id] = make(map[string]context.CancelFunc)
|
snap.WatchedGateways[id] = make(map[string]context.CancelFunc)
|
||||||
}
|
}
|
||||||
if _, ok := snap.ConnectProxy.WatchedGatewayEndpoints[id]; !ok {
|
if _, ok := snap.WatchedGatewayEndpoints[id]; !ok {
|
||||||
snap.ConnectProxy.WatchedGatewayEndpoints[id] = make(map[string]structs.CheckServiceNodes)
|
snap.WatchedGatewayEndpoints[id] = make(map[string]structs.CheckServiceNodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We could invalidate this selectively based on a hash of the relevant
|
// We could invalidate this selectively based on a hash of the relevant
|
||||||
|
@ -697,14 +758,14 @@ func (s *state) resetWatchesFromChain(
|
||||||
// upstream when the chain changes in any way.
|
// upstream when the chain changes in any way.
|
||||||
//
|
//
|
||||||
// TODO(rb): content hash based add/remove
|
// TODO(rb): content hash based add/remove
|
||||||
for targetID, cancelFn := range snap.ConnectProxy.WatchedUpstreams[id] {
|
for targetID, cancelFn := range snap.WatchedUpstreams[id] {
|
||||||
s.logger.Trace("stopping watch of target",
|
s.logger.Trace("stopping watch of target",
|
||||||
"upstream", id,
|
"upstream", id,
|
||||||
"chain", chain.ServiceName,
|
"chain", chain.ServiceName,
|
||||||
"target", targetID,
|
"target", targetID,
|
||||||
)
|
)
|
||||||
delete(snap.ConnectProxy.WatchedUpstreams[id], targetID)
|
delete(snap.WatchedUpstreams[id], targetID)
|
||||||
delete(snap.ConnectProxy.WatchedUpstreamEndpoints[id], targetID)
|
delete(snap.WatchedUpstreamEndpoints[id], targetID)
|
||||||
cancelFn()
|
cancelFn()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -740,11 +801,11 @@ func (s *state) resetWatchesFromChain(
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
snap.ConnectProxy.WatchedUpstreams[id][target.ID] = cancel
|
snap.WatchedUpstreams[id][target.ID] = cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
for dc, _ := range needGateways {
|
for dc, _ := range needGateways {
|
||||||
if _, ok := snap.ConnectProxy.WatchedGateways[id][dc]; ok {
|
if _, ok := snap.WatchedGateways[id][dc]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -761,10 +822,10 @@ func (s *state) resetWatchesFromChain(
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
snap.ConnectProxy.WatchedGateways[id][dc] = cancel
|
snap.WatchedGateways[id][dc] = cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
for dc, cancelFn := range snap.ConnectProxy.WatchedGateways[id] {
|
for dc, cancelFn := range snap.WatchedGateways[id] {
|
||||||
if _, ok := needGateways[dc]; ok {
|
if _, ok := needGateways[dc]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -773,8 +834,8 @@ func (s *state) resetWatchesFromChain(
|
||||||
"chain", chain.ServiceName,
|
"chain", chain.ServiceName,
|
||||||
"datacenter", dc,
|
"datacenter", dc,
|
||||||
)
|
)
|
||||||
delete(snap.ConnectProxy.WatchedGateways[id], dc)
|
delete(snap.WatchedGateways[id], dc)
|
||||||
delete(snap.ConnectProxy.WatchedGatewayEndpoints[id], dc)
|
delete(snap.WatchedGatewayEndpoints[id], dc)
|
||||||
cancelFn()
|
cancelFn()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -969,6 +1030,89 @@ func (s *state) handleUpdateMeshGateway(u cache.UpdateEvent, snap *ConfigSnapsho
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *state) handleUpdateIngressGateway(u cache.UpdateEvent, snap *ConfigSnapshot) error {
|
||||||
|
if u.Err != nil {
|
||||||
|
return fmt.Errorf("error filling agent cache: %v", u.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case u.CorrelationID == rootsWatchID:
|
||||||
|
roots, ok := u.Result.(*structs.IndexedCARoots)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid type for response: %T", u.Result)
|
||||||
|
}
|
||||||
|
snap.Roots = roots
|
||||||
|
case u.CorrelationID == gatewayServicesWatchID:
|
||||||
|
services, ok := u.Result.(*structs.IndexedGatewayServices)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid type for response: %T", u.Result)
|
||||||
|
}
|
||||||
|
|
||||||
|
var upstreams structs.Upstreams
|
||||||
|
watchedSvcs := make(map[string]struct{})
|
||||||
|
for _, service := range services.Services {
|
||||||
|
u := makeUpstream(service, s.address)
|
||||||
|
|
||||||
|
err := s.watchIngressDiscoveryChain(snap, u)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
watchedSvcs[u.Identifier()] = struct{}{}
|
||||||
|
upstreams = append(upstreams, u)
|
||||||
|
}
|
||||||
|
snap.IngressGateway.Upstreams = upstreams
|
||||||
|
|
||||||
|
for id, cancelFn := range snap.IngressGateway.WatchedDiscoveryChains {
|
||||||
|
if _, ok := watchedSvcs[id]; !ok {
|
||||||
|
cancelFn()
|
||||||
|
delete(snap.IngressGateway.WatchedDiscoveryChains, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return s.handleUpdateUpstreams(u, &snap.IngressGateway.ConfigSnapshotUpstreams)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUpstream(g *structs.GatewayService, bindAddr string) structs.Upstream {
|
||||||
|
upstream := structs.Upstream{
|
||||||
|
DestinationName: g.Service.ID,
|
||||||
|
DestinationNamespace: g.Service.NamespaceOrDefault(),
|
||||||
|
LocalBindPort: g.Port,
|
||||||
|
}
|
||||||
|
upstream.LocalBindAddress = bindAddr
|
||||||
|
if bindAddr == "" {
|
||||||
|
upstream.LocalBindAddress = "0.0.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
return upstream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) watchIngressDiscoveryChain(snap *ConfigSnapshot, u structs.Upstream) error {
|
||||||
|
if _, ok := snap.IngressGateway.WatchedDiscoveryChains[u.Identifier()]; ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(s.ctx)
|
||||||
|
err := s.cache.Notify(ctx, cachetype.CompiledDiscoveryChainName, &structs.DiscoveryChainRequest{
|
||||||
|
Datacenter: s.source.Datacenter,
|
||||||
|
QueryOptions: structs.QueryOptions{Token: s.token},
|
||||||
|
Name: u.DestinationName,
|
||||||
|
EvaluateInDatacenter: s.source.Datacenter,
|
||||||
|
EvaluateInNamespace: u.DestinationNamespace,
|
||||||
|
// TODO(ingress): Deal with MeshGateway and Protocol overrides here
|
||||||
|
}, "discovery-chain:"+u.Identifier(), s.ch)
|
||||||
|
if err != nil {
|
||||||
|
cancel()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
snap.IngressGateway.WatchedDiscoveryChains[u.Identifier()] = cancel
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// CurrentSnapshot synchronously returns the current ConfigSnapshot if there is
|
// CurrentSnapshot synchronously returns the current ConfigSnapshot if there is
|
||||||
// one ready. If we don't have one yet because not all necessary parts have been
|
// one ready. If we don't have one yet because not all necessary parts have been
|
||||||
// returned (i.e. both roots and leaf cert), nil is returned.
|
// returned (i.e. both roots and leaf cert), nil is returned.
|
||||||
|
|
|
@ -665,6 +665,139 @@ func TestState_WatchesAndUpdates(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"ingress-gateway": testCase{
|
||||||
|
ns: structs.NodeService{
|
||||||
|
Kind: structs.ServiceKindIngressGateway,
|
||||||
|
ID: "ingress-gateway",
|
||||||
|
Service: "ingress-gateway",
|
||||||
|
Address: "10.0.1.1",
|
||||||
|
},
|
||||||
|
sourceDC: "dc1",
|
||||||
|
stages: []verificationStage{
|
||||||
|
verificationStage{
|
||||||
|
requiredWatches: map[string]verifyWatchRequest{
|
||||||
|
rootsWatchID: genVerifyRootsWatch("dc1"),
|
||||||
|
leafWatchID: genVerifyLeafWatch("ingress-gateway", "dc1"),
|
||||||
|
},
|
||||||
|
verifySnapshot: func(t testing.TB, snap *ConfigSnapshot) {
|
||||||
|
require.False(t, snap.Valid(), "gateway without root is not valid")
|
||||||
|
require.True(t, snap.IngressGateway.IsEmpty())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verificationStage{
|
||||||
|
events: []cache.UpdateEvent{
|
||||||
|
rootWatchEvent(),
|
||||||
|
},
|
||||||
|
verifySnapshot: func(t testing.TB, snap *ConfigSnapshot) {
|
||||||
|
require.False(t, snap.Valid(), "gateway without leaf is not valid")
|
||||||
|
require.Equal(t, indexedRoots, snap.Roots)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verificationStage{
|
||||||
|
events: []cache.UpdateEvent{
|
||||||
|
cache.UpdateEvent{
|
||||||
|
CorrelationID: leafWatchID,
|
||||||
|
Result: issuedCert,
|
||||||
|
Err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verifySnapshot: func(t testing.TB, snap *ConfigSnapshot) {
|
||||||
|
require.True(t, snap.Valid(), "gateway with root and leaf certs is valid")
|
||||||
|
require.Equal(t, issuedCert, snap.IngressGateway.Leaf)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verificationStage{
|
||||||
|
events: []cache.UpdateEvent{
|
||||||
|
cache.UpdateEvent{
|
||||||
|
CorrelationID: gatewayServicesWatchID,
|
||||||
|
Result: &structs.IndexedGatewayServices{
|
||||||
|
Services: structs.GatewayServices{
|
||||||
|
{
|
||||||
|
Gateway: structs.NewServiceID("ingress-gateway", nil),
|
||||||
|
Service: structs.NewServiceID("api", nil),
|
||||||
|
Port: 9999,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verifySnapshot: func(t testing.TB, snap *ConfigSnapshot) {
|
||||||
|
require.Len(t, snap.IngressGateway.Upstreams, 1)
|
||||||
|
require.Len(t, snap.IngressGateway.WatchedDiscoveryChains, 1)
|
||||||
|
require.Contains(t, snap.IngressGateway.WatchedDiscoveryChains, "api")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verificationStage{
|
||||||
|
requiredWatches: map[string]verifyWatchRequest{
|
||||||
|
"discovery-chain:api": genVerifyDiscoveryChainWatch(&structs.DiscoveryChainRequest{
|
||||||
|
Name: "api",
|
||||||
|
EvaluateInDatacenter: "dc1",
|
||||||
|
EvaluateInNamespace: "default",
|
||||||
|
Datacenter: "dc1",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
events: []cache.UpdateEvent{
|
||||||
|
cache.UpdateEvent{
|
||||||
|
CorrelationID: "discovery-chain:api",
|
||||||
|
Result: &structs.DiscoveryChainResponse{
|
||||||
|
Chain: discoverychain.TestCompileConfigEntries(t, "api", "default", "dc1", "trustdomain.consul", "dc1", nil),
|
||||||
|
},
|
||||||
|
Err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verifySnapshot: func(t testing.TB, snap *ConfigSnapshot) {
|
||||||
|
require.Len(t, snap.IngressGateway.WatchedUpstreams, 1)
|
||||||
|
require.Len(t, snap.IngressGateway.WatchedUpstreams["api"], 1)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verificationStage{
|
||||||
|
requiredWatches: map[string]verifyWatchRequest{
|
||||||
|
"upstream-target:api.default.dc1:api": genVerifyServiceWatch("api", "", "dc1", true),
|
||||||
|
},
|
||||||
|
events: []cache.UpdateEvent{
|
||||||
|
cache.UpdateEvent{
|
||||||
|
CorrelationID: "upstream-target:api.default.dc1:api",
|
||||||
|
Result: &structs.IndexedCheckServiceNodes{
|
||||||
|
Nodes: structs.CheckServiceNodes{
|
||||||
|
{
|
||||||
|
Node: &structs.Node{
|
||||||
|
Node: "node1",
|
||||||
|
Address: "127.0.0.1",
|
||||||
|
},
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
ID: "api1",
|
||||||
|
Service: "api",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verifySnapshot: func(t testing.TB, snap *ConfigSnapshot) {
|
||||||
|
require.Len(t, snap.IngressGateway.WatchedUpstreamEndpoints, 1)
|
||||||
|
require.Contains(t, snap.IngressGateway.WatchedUpstreamEndpoints, "api")
|
||||||
|
require.Len(t, snap.IngressGateway.WatchedUpstreamEndpoints["api"], 1)
|
||||||
|
require.Contains(t, snap.IngressGateway.WatchedUpstreamEndpoints["api"], "api.default.dc1")
|
||||||
|
require.Equal(t, snap.IngressGateway.WatchedUpstreamEndpoints["api"]["api.default.dc1"],
|
||||||
|
structs.CheckServiceNodes{
|
||||||
|
{
|
||||||
|
Node: &structs.Node{
|
||||||
|
Node: "node1",
|
||||||
|
Address: "127.0.0.1",
|
||||||
|
},
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
ID: "api1",
|
||||||
|
Service: "api",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
"connect-proxy": newConnectProxyCase(structs.MeshGatewayModeDefault),
|
"connect-proxy": newConnectProxyCase(structs.MeshGatewayModeDefault),
|
||||||
"connect-proxy-mesh-gateway-local": newConnectProxyCase(structs.MeshGatewayModeLocal),
|
"connect-proxy-mesh-gateway-local": newConnectProxyCase(structs.MeshGatewayModeLocal),
|
||||||
}
|
}
|
||||||
|
|
|
@ -586,18 +586,20 @@ func TestConfigSnapshot(t testing.T) *ConfigSnapshot {
|
||||||
},
|
},
|
||||||
Roots: roots,
|
Roots: roots,
|
||||||
ConnectProxy: configSnapshotConnectProxy{
|
ConnectProxy: configSnapshotConnectProxy{
|
||||||
Leaf: leaf,
|
ConfigSnapshotUpstreams: ConfigSnapshotUpstreams{
|
||||||
DiscoveryChain: map[string]*structs.CompiledDiscoveryChain{
|
Leaf: leaf,
|
||||||
"db": dbChain,
|
DiscoveryChain: map[string]*structs.CompiledDiscoveryChain{
|
||||||
|
"db": dbChain,
|
||||||
|
},
|
||||||
|
WatchedUpstreamEndpoints: map[string]map[string]structs.CheckServiceNodes{
|
||||||
|
"db": map[string]structs.CheckServiceNodes{
|
||||||
|
"db.default.dc1": TestUpstreamNodes(t),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
PreparedQueryEndpoints: map[string]structs.CheckServiceNodes{
|
PreparedQueryEndpoints: map[string]structs.CheckServiceNodes{
|
||||||
"prepared_query:geo-cache": TestUpstreamNodes(t),
|
"prepared_query:geo-cache": TestUpstreamNodes(t),
|
||||||
},
|
},
|
||||||
WatchedUpstreamEndpoints: map[string]map[string]structs.CheckServiceNodes{
|
|
||||||
"db": map[string]structs.CheckServiceNodes{
|
|
||||||
"db.default.dc1": TestUpstreamNodes(t),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
}
|
}
|
||||||
|
@ -881,13 +883,15 @@ func testConfigSnapshotDiscoveryChain(t testing.T, variation string, additionalE
|
||||||
},
|
},
|
||||||
Roots: roots,
|
Roots: roots,
|
||||||
ConnectProxy: configSnapshotConnectProxy{
|
ConnectProxy: configSnapshotConnectProxy{
|
||||||
Leaf: leaf,
|
ConfigSnapshotUpstreams: ConfigSnapshotUpstreams{
|
||||||
DiscoveryChain: map[string]*structs.CompiledDiscoveryChain{
|
Leaf: leaf,
|
||||||
"db": dbChain,
|
DiscoveryChain: map[string]*structs.CompiledDiscoveryChain{
|
||||||
},
|
"db": dbChain,
|
||||||
WatchedUpstreamEndpoints: map[string]map[string]structs.CheckServiceNodes{
|
},
|
||||||
"db": map[string]structs.CheckServiceNodes{
|
WatchedUpstreamEndpoints: map[string]map[string]structs.CheckServiceNodes{
|
||||||
"db.default.dc1": TestUpstreamNodes(t),
|
"db": map[string]structs.CheckServiceNodes{
|
||||||
|
"db.default.dc1": TestUpstreamNodes(t),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1036,6 +1040,54 @@ func testConfigSnapshotMeshGateway(t testing.T, populateServices bool, useFedera
|
||||||
return snap
|
return snap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigSnapshotIngressGateway(t testing.T) *ConfigSnapshot {
|
||||||
|
return testConfigSnapshotIngressGateway(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigSnapshotIngressGatewayNoServices(t testing.T) *ConfigSnapshot {
|
||||||
|
return testConfigSnapshotIngressGateway(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testConfigSnapshotIngressGateway(t testing.T, populateServices bool) *ConfigSnapshot {
|
||||||
|
roots, leaf := TestCerts(t)
|
||||||
|
dbChain := discoverychain.TestCompileConfigEntries(
|
||||||
|
t, "db", "default", "dc1",
|
||||||
|
connect.TestClusterID+".consul", "dc1", nil)
|
||||||
|
|
||||||
|
snap := &ConfigSnapshot{
|
||||||
|
Kind: structs.ServiceKindIngressGateway,
|
||||||
|
Service: "ingress-gateway",
|
||||||
|
ProxyID: structs.NewServiceID("ingress-gateway", nil),
|
||||||
|
Address: "1.2.3.4",
|
||||||
|
Roots: roots,
|
||||||
|
Datacenter: "dc1",
|
||||||
|
}
|
||||||
|
if populateServices {
|
||||||
|
snap.IngressGateway = configSnapshotIngressGateway{
|
||||||
|
ConfigSnapshotUpstreams: ConfigSnapshotUpstreams{
|
||||||
|
Leaf: leaf,
|
||||||
|
DiscoveryChain: map[string]*structs.CompiledDiscoveryChain{
|
||||||
|
"db": dbChain,
|
||||||
|
},
|
||||||
|
WatchedUpstreamEndpoints: map[string]map[string]structs.CheckServiceNodes{
|
||||||
|
"db": map[string]structs.CheckServiceNodes{
|
||||||
|
"db.default.dc1": TestUpstreamNodes(t),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Upstreams: structs.Upstreams{
|
||||||
|
{
|
||||||
|
// We rely on this one having default type in a few tests...
|
||||||
|
DestinationName: "db",
|
||||||
|
LocalBindPort: 9191,
|
||||||
|
LocalBindAddress: "2.3.4.5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return snap
|
||||||
|
}
|
||||||
|
|
||||||
func TestConfigSnapshotExposeConfig(t testing.T) *ConfigSnapshot {
|
func TestConfigSnapshotExposeConfig(t testing.T) *ConfigSnapshot {
|
||||||
return &ConfigSnapshot{
|
return &ConfigSnapshot{
|
||||||
Kind: structs.ServiceKindConnectProxy,
|
Kind: structs.ServiceKindConnectProxy,
|
||||||
|
|
|
@ -77,11 +77,19 @@ func (e *IngressGatewayConfigEntry) Normalize() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
e.Kind = IngressGateway
|
e.Kind = IngressGateway
|
||||||
for _, listener := range e.Listeners {
|
for i, listener := range e.Listeners {
|
||||||
|
if listener.Protocol == "" {
|
||||||
|
listener.Protocol = "tcp"
|
||||||
|
}
|
||||||
|
|
||||||
listener.Protocol = strings.ToLower(listener.Protocol)
|
listener.Protocol = strings.ToLower(listener.Protocol)
|
||||||
for i := range listener.Services {
|
for i := range listener.Services {
|
||||||
listener.Services[i].EnterpriseMeta.Normalize()
|
listener.Services[i].EnterpriseMeta.Normalize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure to set the item back into the array, since we are not using
|
||||||
|
// pointers to structs
|
||||||
|
e.Listeners[i] = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
e.EnterpriseMeta.Normalize()
|
e.EnterpriseMeta.Normalize()
|
||||||
|
@ -135,7 +143,7 @@ func (e *IngressGatewayConfigEntry) Validate() error {
|
||||||
func (e *IngressGatewayConfigEntry) CanRead(authz acl.Authorizer) bool {
|
func (e *IngressGatewayConfigEntry) CanRead(authz acl.Authorizer) bool {
|
||||||
var authzContext acl.AuthorizerContext
|
var authzContext acl.AuthorizerContext
|
||||||
e.FillAuthzContext(&authzContext)
|
e.FillAuthzContext(&authzContext)
|
||||||
return authz.OperatorRead(&authzContext) == acl.Allow
|
return authz.ServiceRead(e.Name, &authzContext) == acl.Allow
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *IngressGatewayConfigEntry) CanWrite(authz acl.Authorizer) bool {
|
func (e *IngressGatewayConfigEntry) CanWrite(authz acl.Authorizer) bool {
|
||||||
|
@ -160,6 +168,10 @@ func (e *IngressGatewayConfigEntry) GetEnterpriseMeta() *EnterpriseMeta {
|
||||||
return &e.EnterpriseMeta
|
return &e.EnterpriseMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *IngressService) ToServiceID() ServiceID {
|
||||||
|
return NewServiceID(s.Name, &s.EnterpriseMeta)
|
||||||
|
}
|
||||||
|
|
||||||
// TerminatingGatewayConfigEntry manages the configuration for a terminating service
|
// TerminatingGatewayConfigEntry manages the configuration for a terminating service
|
||||||
// with the given name.
|
// with the given name.
|
||||||
type TerminatingGatewayConfigEntry struct {
|
type TerminatingGatewayConfigEntry struct {
|
||||||
|
@ -283,9 +295,11 @@ type GatewayService struct {
|
||||||
Gateway ServiceID
|
Gateway ServiceID
|
||||||
Service ServiceID
|
Service ServiceID
|
||||||
GatewayKind ServiceKind
|
GatewayKind ServiceKind
|
||||||
|
Port int
|
||||||
CAFile string
|
CAFile string
|
||||||
CertFile string
|
CertFile string
|
||||||
KeyFile string
|
KeyFile string
|
||||||
|
RaftIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
type GatewayServices []*GatewayService
|
type GatewayServices []*GatewayService
|
||||||
|
@ -294,7 +308,21 @@ func (g *GatewayService) IsSame(o *GatewayService) bool {
|
||||||
return g.Gateway.Matches(&o.Gateway) &&
|
return g.Gateway.Matches(&o.Gateway) &&
|
||||||
g.Service.Matches(&o.Service) &&
|
g.Service.Matches(&o.Service) &&
|
||||||
g.GatewayKind == o.GatewayKind &&
|
g.GatewayKind == o.GatewayKind &&
|
||||||
|
g.Port == o.Port &&
|
||||||
g.CAFile == o.CAFile &&
|
g.CAFile == o.CAFile &&
|
||||||
g.CertFile == o.CertFile &&
|
g.CertFile == o.CertFile &&
|
||||||
g.KeyFile == o.KeyFile
|
g.KeyFile == o.KeyFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *GatewayService) Clone() *GatewayService {
|
||||||
|
return &GatewayService{
|
||||||
|
Gateway: g.Gateway,
|
||||||
|
Service: g.Service,
|
||||||
|
GatewayKind: g.GatewayKind,
|
||||||
|
Port: g.Port,
|
||||||
|
CAFile: g.CAFile,
|
||||||
|
CertFile: g.CertFile,
|
||||||
|
KeyFile: g.KeyFile,
|
||||||
|
RaftIndex: g.RaftIndex,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,89 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestIngressConfigEntry_Normalize(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
entry IngressGatewayConfigEntry
|
||||||
|
expected IngressGatewayConfigEntry
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty protocol",
|
||||||
|
entry: IngressGatewayConfigEntry{
|
||||||
|
Kind: "ingress-gateway",
|
||||||
|
Name: "ingress-web",
|
||||||
|
Listeners: []IngressListener{
|
||||||
|
{
|
||||||
|
Port: 1111,
|
||||||
|
Protocol: "",
|
||||||
|
Services: []IngressService{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: IngressGatewayConfigEntry{
|
||||||
|
Kind: "ingress-gateway",
|
||||||
|
Name: "ingress-web",
|
||||||
|
Listeners: []IngressListener{
|
||||||
|
{
|
||||||
|
Port: 1111,
|
||||||
|
Protocol: "tcp",
|
||||||
|
Services: []IngressService{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "lowercase protocols",
|
||||||
|
entry: IngressGatewayConfigEntry{
|
||||||
|
Kind: "ingress-gateway",
|
||||||
|
Name: "ingress-web",
|
||||||
|
Listeners: []IngressListener{
|
||||||
|
{
|
||||||
|
Port: 1111,
|
||||||
|
Protocol: "TCP",
|
||||||
|
Services: []IngressService{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Port: 1112,
|
||||||
|
Protocol: "HtTP",
|
||||||
|
Services: []IngressService{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: IngressGatewayConfigEntry{
|
||||||
|
Kind: "ingress-gateway",
|
||||||
|
Name: "ingress-web",
|
||||||
|
Listeners: []IngressListener{
|
||||||
|
{
|
||||||
|
Port: 1111,
|
||||||
|
Protocol: "tcp",
|
||||||
|
Services: []IngressService{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Port: 1112,
|
||||||
|
Protocol: "http",
|
||||||
|
Services: []IngressService{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range cases {
|
||||||
|
// We explicitly copy the variable for the range statement so that can run
|
||||||
|
// tests in parallel.
|
||||||
|
tc := test
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
err := tc.entry.Normalize()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.expected, tc.entry)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestIngressConfigEntry_Validate(t *testing.T) {
|
func TestIngressConfigEntry_Validate(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
@ -333,6 +416,7 @@ func TestTerminatingConfigEntry_Validate(t *testing.T) {
|
||||||
tc := test
|
tc := test
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
err := tc.entry.Validate()
|
err := tc.entry.Validate()
|
||||||
if tc.expectErr != "" {
|
if tc.expectErr != "" {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
|
@ -502,7 +502,6 @@ type ServiceSpecificRequest struct {
|
||||||
Datacenter string
|
Datacenter string
|
||||||
NodeMetaFilters map[string]string
|
NodeMetaFilters map[string]string
|
||||||
ServiceName string
|
ServiceName string
|
||||||
ServiceKind ServiceKind
|
|
||||||
// DEPRECATED (singular-service-tag) - remove this when backwards RPC compat
|
// DEPRECATED (singular-service-tag) - remove this when backwards RPC compat
|
||||||
// with 1.2.x is not required.
|
// with 1.2.x is not required.
|
||||||
ServiceTag string
|
ServiceTag string
|
||||||
|
@ -514,6 +513,12 @@ type ServiceSpecificRequest struct {
|
||||||
// Connect if true will only search for Connect-compatible services.
|
// Connect if true will only search for Connect-compatible services.
|
||||||
Connect bool
|
Connect bool
|
||||||
|
|
||||||
|
// TODO(ingress): Add corresponding API changes after figuring out what the
|
||||||
|
// HTTP endpoint looks like
|
||||||
|
|
||||||
|
// Ingress if true will only search for Ingress gateways for the given service.
|
||||||
|
Ingress bool
|
||||||
|
|
||||||
EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
|
EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
|
||||||
QueryOptions
|
QueryOptions
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,17 @@ func TestRegisterRequestProxy(t testing.T) *RegisterRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRegisterIngressGateway returns a RegisterRequest for registering an
|
||||||
|
// ingress gateway
|
||||||
|
func TestRegisterIngressGateway(t testing.T) *RegisterRequest {
|
||||||
|
return &RegisterRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foo",
|
||||||
|
Address: "127.0.0.1",
|
||||||
|
Service: TestNodeServiceIngressGateway(t, ""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestNodeService returns a *NodeService representing a valid regular service.
|
// TestNodeService returns a *NodeService representing a valid regular service.
|
||||||
func TestNodeService(t testing.T) *NodeService {
|
func TestNodeService(t testing.T) *NodeService {
|
||||||
return &NodeService{
|
return &NodeService{
|
||||||
|
|
|
@ -32,6 +32,8 @@ func (s *Server) clustersFromSnapshot(cfgSnap *proxycfg.ConfigSnapshot, _ string
|
||||||
return s.clustersFromSnapshotConnectProxy(cfgSnap)
|
return s.clustersFromSnapshotConnectProxy(cfgSnap)
|
||||||
case structs.ServiceKindMeshGateway:
|
case structs.ServiceKindMeshGateway:
|
||||||
return s.clustersFromSnapshotMeshGateway(cfgSnap)
|
return s.clustersFromSnapshotMeshGateway(cfgSnap)
|
||||||
|
case structs.ServiceKindIngressGateway:
|
||||||
|
return s.clustersFromSnapshotIngressGateway(cfgSnap)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("Invalid service kind: %v", cfgSnap.Kind)
|
return nil, fmt.Errorf("Invalid service kind: %v", cfgSnap.Kind)
|
||||||
}
|
}
|
||||||
|
@ -63,7 +65,13 @@ func (s *Server) clustersFromSnapshotConnectProxy(cfgSnap *proxycfg.ConfigSnapsh
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
chain := cfgSnap.ConnectProxy.DiscoveryChain[id]
|
chain := cfgSnap.ConnectProxy.DiscoveryChain[id]
|
||||||
upstreamClusters, err := s.makeUpstreamClustersForDiscoveryChain(u, chain, cfgSnap)
|
chainEndpoints, ok := cfgSnap.ConnectProxy.WatchedUpstreamEndpoints[id]
|
||||||
|
if !ok {
|
||||||
|
// this should not happen
|
||||||
|
return nil, fmt.Errorf("no endpoint map for upstream %q", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
upstreamClusters, err := s.makeUpstreamClustersForDiscoveryChain(u, chain, chainEndpoints, cfgSnap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -192,6 +200,34 @@ func (s *Server) clustersFromSnapshotMeshGateway(cfgSnap *proxycfg.ConfigSnapsho
|
||||||
return clusters, nil
|
return clusters, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) clustersFromSnapshotIngressGateway(cfgSnap *proxycfg.ConfigSnapshot) ([]proto.Message, error) {
|
||||||
|
var clusters []proto.Message
|
||||||
|
for _, u := range cfgSnap.IngressGateway.Upstreams {
|
||||||
|
id := u.Identifier()
|
||||||
|
chain, ok := cfgSnap.IngressGateway.DiscoveryChain[id]
|
||||||
|
if !ok {
|
||||||
|
// this should not happen
|
||||||
|
return nil, fmt.Errorf("no discovery chain for upstream %q", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
chainEndpoints, ok := cfgSnap.IngressGateway.WatchedUpstreamEndpoints[id]
|
||||||
|
if !ok {
|
||||||
|
// this should not happen
|
||||||
|
return nil, fmt.Errorf("no endpoint map for upstream %q", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
upstreamClusters, err := s.makeUpstreamClustersForDiscoveryChain(u, chain, chainEndpoints, cfgSnap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range upstreamClusters {
|
||||||
|
clusters = append(clusters, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clusters, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) makeAppCluster(cfgSnap *proxycfg.ConfigSnapshot, name, pathProtocol string, port int) (*envoy.Cluster, error) {
|
func (s *Server) makeAppCluster(cfgSnap *proxycfg.ConfigSnapshot, name, pathProtocol string, port int) (*envoy.Cluster, error) {
|
||||||
var c *envoy.Cluster
|
var c *envoy.Cluster
|
||||||
var err error
|
var err error
|
||||||
|
@ -299,6 +335,7 @@ func (s *Server) makeUpstreamClusterForPreparedQuery(upstream structs.Upstream,
|
||||||
func (s *Server) makeUpstreamClustersForDiscoveryChain(
|
func (s *Server) makeUpstreamClustersForDiscoveryChain(
|
||||||
upstream structs.Upstream,
|
upstream structs.Upstream,
|
||||||
chain *structs.CompiledDiscoveryChain,
|
chain *structs.CompiledDiscoveryChain,
|
||||||
|
chainEndpoints map[string]structs.CheckServiceNodes,
|
||||||
cfgSnap *proxycfg.ConfigSnapshot,
|
cfgSnap *proxycfg.ConfigSnapshot,
|
||||||
) ([]*envoy.Cluster, error) {
|
) ([]*envoy.Cluster, error) {
|
||||||
if chain == nil {
|
if chain == nil {
|
||||||
|
@ -329,15 +366,7 @@ func (s *Server) makeUpstreamClustersForDiscoveryChain(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
id := upstream.Identifier()
|
|
||||||
chainEndpointMap, ok := cfgSnap.ConnectProxy.WatchedUpstreamEndpoints[id]
|
|
||||||
if !ok {
|
|
||||||
// this should not happen
|
|
||||||
return nil, fmt.Errorf("no endpoint map for upstream %q", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
var out []*envoy.Cluster
|
var out []*envoy.Cluster
|
||||||
|
|
||||||
for _, node := range chain.Nodes {
|
for _, node := range chain.Nodes {
|
||||||
if node.Type != structs.DiscoveryGraphNodeTypeResolver {
|
if node.Type != structs.DiscoveryGraphNodeTypeResolver {
|
||||||
continue
|
continue
|
||||||
|
@ -356,7 +385,7 @@ func (s *Server) makeUpstreamClustersForDiscoveryChain(
|
||||||
if failoverThroughMeshGateway {
|
if failoverThroughMeshGateway {
|
||||||
actualTargetID := firstHealthyTarget(
|
actualTargetID := firstHealthyTarget(
|
||||||
chain.Targets,
|
chain.Targets,
|
||||||
chainEndpointMap,
|
chainEndpoints,
|
||||||
targetID,
|
targetID,
|
||||||
failover.Targets,
|
failover.Targets,
|
||||||
)
|
)
|
||||||
|
|
|
@ -343,6 +343,16 @@ func TestClustersFromSnapshot(t *testing.T) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "ingress-gateway",
|
||||||
|
create: proxycfg.TestConfigSnapshotIngressGateway,
|
||||||
|
setup: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ingress-gateway-no-services",
|
||||||
|
create: proxycfg.TestConfigSnapshotIngressGatewayNoServices,
|
||||||
|
setup: nil,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@ -355,13 +365,7 @@ func TestClustersFromSnapshot(t *testing.T) {
|
||||||
// We need to replace the TLS certs with deterministic ones to make golden
|
// We need to replace the TLS certs with deterministic ones to make golden
|
||||||
// files workable. Note we don't update these otherwise they'd change
|
// files workable. Note we don't update these otherwise they'd change
|
||||||
// golder files for every test case and so not be any use!
|
// golder files for every test case and so not be any use!
|
||||||
if snap.ConnectProxy.Leaf != nil {
|
setupTLSRootsAndLeaf(t, snap)
|
||||||
snap.ConnectProxy.Leaf.CertPEM = golden(t, "test-leaf-cert", "")
|
|
||||||
snap.ConnectProxy.Leaf.PrivateKeyPEM = golden(t, "test-leaf-key", "")
|
|
||||||
}
|
|
||||||
if snap.Roots != nil {
|
|
||||||
snap.Roots.Roots[0].RootCert = golden(t, "test-root-cert", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.setup != nil {
|
if tt.setup != nil {
|
||||||
tt.setup(snap)
|
tt.setup(snap)
|
||||||
|
@ -537,3 +541,19 @@ func customAppClusterJSON(t *testing.T, opts customClusterJSONOptions) string {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupTLSRootsAndLeaf(t *testing.T, snap *proxycfg.ConfigSnapshot) {
|
||||||
|
if snap.Leaf() != nil {
|
||||||
|
switch snap.Kind {
|
||||||
|
case structs.ServiceKindConnectProxy:
|
||||||
|
snap.ConnectProxy.Leaf.CertPEM = golden(t, "test-leaf-cert", "")
|
||||||
|
snap.ConnectProxy.Leaf.PrivateKeyPEM = golden(t, "test-leaf-key", "")
|
||||||
|
case structs.ServiceKindIngressGateway:
|
||||||
|
snap.IngressGateway.Leaf.CertPEM = golden(t, "test-leaf-cert", "")
|
||||||
|
snap.IngressGateway.Leaf.PrivateKeyPEM = golden(t, "test-leaf-key", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if snap.Roots != nil {
|
||||||
|
snap.Roots.Roots[0].RootCert = golden(t, "test-root-cert", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -32,6 +32,8 @@ func (s *Server) endpointsFromSnapshot(cfgSnap *proxycfg.ConfigSnapshot, _ strin
|
||||||
return s.endpointsFromSnapshotConnectProxy(cfgSnap)
|
return s.endpointsFromSnapshotConnectProxy(cfgSnap)
|
||||||
case structs.ServiceKindMeshGateway:
|
case structs.ServiceKindMeshGateway:
|
||||||
return s.endpointsFromSnapshotMeshGateway(cfgSnap)
|
return s.endpointsFromSnapshotMeshGateway(cfgSnap)
|
||||||
|
case structs.ServiceKindIngressGateway:
|
||||||
|
return s.endpointsFromSnapshotIngressGateway(cfgSnap)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("Invalid service kind: %v", cfgSnap.Kind)
|
return nil, fmt.Errorf("Invalid service kind: %v", cfgSnap.Kind)
|
||||||
}
|
}
|
||||||
|
@ -74,79 +76,13 @@ func (s *Server) endpointsFromSnapshotConnectProxy(cfgSnap *proxycfg.ConfigSnaps
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Newfangled discovery chain plumbing.
|
// Newfangled discovery chain plumbing.
|
||||||
|
es := s.endpointsFromDiscoveryChain(
|
||||||
// Find all resolver nodes.
|
chain,
|
||||||
for _, node := range chain.Nodes {
|
cfgSnap.Datacenter,
|
||||||
if node.Type != structs.DiscoveryGraphNodeTypeResolver {
|
cfgSnap.ConnectProxy.WatchedUpstreamEndpoints[id],
|
||||||
continue
|
cfgSnap.ConnectProxy.WatchedGatewayEndpoints[id],
|
||||||
}
|
)
|
||||||
failover := node.Resolver.Failover
|
resources = append(resources, es...)
|
||||||
targetID := node.Resolver.Target
|
|
||||||
|
|
||||||
target := chain.Targets[targetID]
|
|
||||||
|
|
||||||
clusterName := CustomizeClusterName(target.Name, chain)
|
|
||||||
|
|
||||||
// Determine if we have to generate the entire cluster differently.
|
|
||||||
failoverThroughMeshGateway := chain.WillFailoverThroughMeshGateway(node)
|
|
||||||
|
|
||||||
if failoverThroughMeshGateway {
|
|
||||||
actualTargetID := firstHealthyTarget(
|
|
||||||
chain.Targets,
|
|
||||||
cfgSnap.ConnectProxy.WatchedUpstreamEndpoints[id],
|
|
||||||
targetID,
|
|
||||||
failover.Targets,
|
|
||||||
)
|
|
||||||
if actualTargetID != targetID {
|
|
||||||
targetID = actualTargetID
|
|
||||||
target = chain.Targets[actualTargetID]
|
|
||||||
}
|
|
||||||
|
|
||||||
failover = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
primaryGroup, valid := makeLoadAssignmentEndpointGroup(
|
|
||||||
chain.Targets,
|
|
||||||
cfgSnap.ConnectProxy.WatchedUpstreamEndpoints[id],
|
|
||||||
cfgSnap.ConnectProxy.WatchedGatewayEndpoints[id],
|
|
||||||
targetID,
|
|
||||||
cfgSnap.Datacenter,
|
|
||||||
)
|
|
||||||
if !valid {
|
|
||||||
continue // skip the cluster if we're still populating the snapshot
|
|
||||||
}
|
|
||||||
|
|
||||||
var endpointGroups []loadAssignmentEndpointGroup
|
|
||||||
|
|
||||||
if failover != nil && len(failover.Targets) > 0 {
|
|
||||||
endpointGroups = make([]loadAssignmentEndpointGroup, 0, len(failover.Targets)+1)
|
|
||||||
|
|
||||||
endpointGroups = append(endpointGroups, primaryGroup)
|
|
||||||
|
|
||||||
for _, failTargetID := range failover.Targets {
|
|
||||||
failoverGroup, valid := makeLoadAssignmentEndpointGroup(
|
|
||||||
chain.Targets,
|
|
||||||
cfgSnap.ConnectProxy.WatchedUpstreamEndpoints[id],
|
|
||||||
cfgSnap.ConnectProxy.WatchedGatewayEndpoints[id],
|
|
||||||
failTargetID,
|
|
||||||
cfgSnap.Datacenter,
|
|
||||||
)
|
|
||||||
if !valid {
|
|
||||||
continue // skip the failover target if we're still populating the snapshot
|
|
||||||
}
|
|
||||||
endpointGroups = append(endpointGroups, failoverGroup)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
endpointGroups = append(endpointGroups, primaryGroup)
|
|
||||||
}
|
|
||||||
|
|
||||||
la := makeLoadAssignment(
|
|
||||||
clusterName,
|
|
||||||
endpointGroups,
|
|
||||||
cfgSnap.Datacenter,
|
|
||||||
)
|
|
||||||
resources = append(resources, la)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,6 +233,22 @@ func (s *Server) endpointsFromSnapshotMeshGateway(cfgSnap *proxycfg.ConfigSnapsh
|
||||||
return resources, nil
|
return resources, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) endpointsFromSnapshotIngressGateway(cfgSnap *proxycfg.ConfigSnapshot) ([]proto.Message, error) {
|
||||||
|
var resources []proto.Message
|
||||||
|
for _, u := range cfgSnap.IngressGateway.Upstreams {
|
||||||
|
id := u.Identifier()
|
||||||
|
|
||||||
|
es := s.endpointsFromDiscoveryChain(
|
||||||
|
cfgSnap.IngressGateway.DiscoveryChain[id],
|
||||||
|
cfgSnap.Datacenter,
|
||||||
|
cfgSnap.IngressGateway.WatchedUpstreamEndpoints[id],
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
resources = append(resources, es...)
|
||||||
|
}
|
||||||
|
return resources, nil
|
||||||
|
}
|
||||||
|
|
||||||
func makeEndpoint(clusterName, host string, port int) envoyendpoint.LbEndpoint {
|
func makeEndpoint(clusterName, host string, port int) envoyendpoint.LbEndpoint {
|
||||||
return envoyendpoint.LbEndpoint{
|
return envoyendpoint.LbEndpoint{
|
||||||
HostIdentifier: &envoyendpoint.LbEndpoint_Endpoint{
|
HostIdentifier: &envoyendpoint.LbEndpoint_Endpoint{
|
||||||
|
@ -307,6 +259,93 @@ func makeEndpoint(clusterName, host string, port int) envoyendpoint.LbEndpoint {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) endpointsFromDiscoveryChain(
|
||||||
|
chain *structs.CompiledDiscoveryChain,
|
||||||
|
datacenter string,
|
||||||
|
upstreamEndpoints, gatewayEndpoints map[string]structs.CheckServiceNodes,
|
||||||
|
) []proto.Message {
|
||||||
|
var resources []proto.Message
|
||||||
|
|
||||||
|
if chain == nil {
|
||||||
|
return resources
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all resolver nodes.
|
||||||
|
for _, node := range chain.Nodes {
|
||||||
|
if node.Type != structs.DiscoveryGraphNodeTypeResolver {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
failover := node.Resolver.Failover
|
||||||
|
targetID := node.Resolver.Target
|
||||||
|
|
||||||
|
target := chain.Targets[targetID]
|
||||||
|
|
||||||
|
clusterName := CustomizeClusterName(target.Name, chain)
|
||||||
|
|
||||||
|
// Determine if we have to generate the entire cluster differently.
|
||||||
|
failoverThroughMeshGateway := chain.WillFailoverThroughMeshGateway(node)
|
||||||
|
|
||||||
|
if failoverThroughMeshGateway {
|
||||||
|
actualTargetID := firstHealthyTarget(
|
||||||
|
chain.Targets,
|
||||||
|
upstreamEndpoints,
|
||||||
|
targetID,
|
||||||
|
failover.Targets,
|
||||||
|
)
|
||||||
|
if actualTargetID != targetID {
|
||||||
|
targetID = actualTargetID
|
||||||
|
target = chain.Targets[actualTargetID]
|
||||||
|
}
|
||||||
|
|
||||||
|
failover = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryGroup, valid := makeLoadAssignmentEndpointGroup(
|
||||||
|
chain.Targets,
|
||||||
|
upstreamEndpoints,
|
||||||
|
gatewayEndpoints,
|
||||||
|
targetID,
|
||||||
|
datacenter,
|
||||||
|
)
|
||||||
|
if !valid {
|
||||||
|
continue // skip the cluster if we're still populating the snapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
var endpointGroups []loadAssignmentEndpointGroup
|
||||||
|
|
||||||
|
if failover != nil && len(failover.Targets) > 0 {
|
||||||
|
endpointGroups = make([]loadAssignmentEndpointGroup, 0, len(failover.Targets)+1)
|
||||||
|
|
||||||
|
endpointGroups = append(endpointGroups, primaryGroup)
|
||||||
|
|
||||||
|
for _, failTargetID := range failover.Targets {
|
||||||
|
failoverGroup, valid := makeLoadAssignmentEndpointGroup(
|
||||||
|
chain.Targets,
|
||||||
|
upstreamEndpoints,
|
||||||
|
gatewayEndpoints,
|
||||||
|
failTargetID,
|
||||||
|
datacenter,
|
||||||
|
)
|
||||||
|
if !valid {
|
||||||
|
continue // skip the failover target if we're still populating the snapshot
|
||||||
|
}
|
||||||
|
endpointGroups = append(endpointGroups, failoverGroup)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
endpointGroups = append(endpointGroups, primaryGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
la := makeLoadAssignment(
|
||||||
|
clusterName,
|
||||||
|
endpointGroups,
|
||||||
|
datacenter,
|
||||||
|
)
|
||||||
|
resources = append(resources, la)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources
|
||||||
|
}
|
||||||
|
|
||||||
type loadAssignmentEndpointGroup struct {
|
type loadAssignmentEndpointGroup struct {
|
||||||
Endpoints structs.CheckServiceNodes
|
Endpoints structs.CheckServiceNodes
|
||||||
OnlyPassing bool
|
OnlyPassing bool
|
||||||
|
|
|
@ -381,6 +381,16 @@ func Test_endpointsFromSnapshot(t *testing.T) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "ingress-gateway",
|
||||||
|
create: proxycfg.TestConfigSnapshotIngressGateway,
|
||||||
|
setup: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ingress-gateway-no-services",
|
||||||
|
create: proxycfg.TestConfigSnapshotIngressGatewayNoServices,
|
||||||
|
setup: nil,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@ -393,13 +403,7 @@ func Test_endpointsFromSnapshot(t *testing.T) {
|
||||||
// We need to replace the TLS certs with deterministic ones to make golden
|
// We need to replace the TLS certs with deterministic ones to make golden
|
||||||
// files workable. Note we don't update these otherwise they'd change
|
// files workable. Note we don't update these otherwise they'd change
|
||||||
// golden files for every test case and so not be any use!
|
// golden files for every test case and so not be any use!
|
||||||
if snap.ConnectProxy.Leaf != nil {
|
setupTLSRootsAndLeaf(t, snap)
|
||||||
snap.ConnectProxy.Leaf.CertPEM = golden(t, "test-leaf-cert", "")
|
|
||||||
snap.ConnectProxy.Leaf.PrivateKeyPEM = golden(t, "test-leaf-key", "")
|
|
||||||
}
|
|
||||||
if snap.Roots != nil {
|
|
||||||
snap.Roots.Roots[0].RootCert = golden(t, "test-root-cert", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.setup != nil {
|
if tt.setup != nil {
|
||||||
tt.setup(snap)
|
tt.setup(snap)
|
||||||
|
|
|
@ -40,6 +40,8 @@ func (s *Server) listenersFromSnapshot(cfgSnap *proxycfg.ConfigSnapshot, token s
|
||||||
return s.listenersFromSnapshotConnectProxy(cfgSnap, token)
|
return s.listenersFromSnapshotConnectProxy(cfgSnap, token)
|
||||||
case structs.ServiceKindMeshGateway:
|
case structs.ServiceKindMeshGateway:
|
||||||
return s.listenersFromSnapshotMeshGateway(cfgSnap)
|
return s.listenersFromSnapshotMeshGateway(cfgSnap)
|
||||||
|
case structs.ServiceKindIngressGateway:
|
||||||
|
return s.listenersFromSnapshotIngressGateway(cfgSnap)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("Invalid service kind: %v", cfgSnap.Kind)
|
return nil, fmt.Errorf("Invalid service kind: %v", cfgSnap.Kind)
|
||||||
}
|
}
|
||||||
|
@ -226,6 +228,34 @@ func (s *Server) listenersFromSnapshotMeshGateway(cfgSnap *proxycfg.ConfigSnapsh
|
||||||
return resources, err
|
return resources, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(ingress): Support configured bind addresses from similar to mesh gateways
|
||||||
|
// See: https://www.consul.io/docs/connect/proxies/envoy.html#mesh-gateway-options
|
||||||
|
func (s *Server) listenersFromSnapshotIngressGateway(cfgSnap *proxycfg.ConfigSnapshot) ([]proto.Message, error) {
|
||||||
|
var resources []proto.Message
|
||||||
|
// TODO(ingress): We give each upstream a distinct listener at the moment,
|
||||||
|
// for http listeners we will need to multiplex upstreams on a single
|
||||||
|
// listener.
|
||||||
|
for _, u := range cfgSnap.IngressGateway.Upstreams {
|
||||||
|
id := u.Identifier()
|
||||||
|
|
||||||
|
chain := cfgSnap.IngressGateway.DiscoveryChain[id]
|
||||||
|
|
||||||
|
var upstreamListener proto.Message
|
||||||
|
var err error
|
||||||
|
if chain == nil || chain.IsDefault() {
|
||||||
|
upstreamListener, err = s.makeUpstreamListenerIgnoreDiscoveryChain(&u, chain, cfgSnap)
|
||||||
|
} else {
|
||||||
|
upstreamListener, err = s.makeUpstreamListenerForDiscoveryChain(&u, chain, cfgSnap)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resources = append(resources, upstreamListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources, nil
|
||||||
|
}
|
||||||
|
|
||||||
// makeListener returns a listener with name and bind details set. Filters must
|
// makeListener returns a listener with name and bind details set. Filters must
|
||||||
// be added before it's useful.
|
// be added before it's useful.
|
||||||
//
|
//
|
||||||
|
@ -862,18 +892,19 @@ func makeCommonTLSContext(cfgSnap *proxycfg.ConfigSnapshot) *envoyauth.CommonTls
|
||||||
rootPEMS += root.RootCert
|
rootPEMS += root.RootCert
|
||||||
}
|
}
|
||||||
|
|
||||||
|
leaf := cfgSnap.Leaf()
|
||||||
return &envoyauth.CommonTlsContext{
|
return &envoyauth.CommonTlsContext{
|
||||||
TlsParams: &envoyauth.TlsParameters{},
|
TlsParams: &envoyauth.TlsParameters{},
|
||||||
TlsCertificates: []*envoyauth.TlsCertificate{
|
TlsCertificates: []*envoyauth.TlsCertificate{
|
||||||
&envoyauth.TlsCertificate{
|
&envoyauth.TlsCertificate{
|
||||||
CertificateChain: &envoycore.DataSource{
|
CertificateChain: &envoycore.DataSource{
|
||||||
Specifier: &envoycore.DataSource_InlineString{
|
Specifier: &envoycore.DataSource_InlineString{
|
||||||
InlineString: cfgSnap.ConnectProxy.Leaf.CertPEM,
|
InlineString: leaf.CertPEM,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
PrivateKey: &envoycore.DataSource{
|
PrivateKey: &envoycore.DataSource{
|
||||||
Specifier: &envoycore.DataSource_InlineString{
|
Specifier: &envoycore.DataSource_InlineString{
|
||||||
InlineString: cfgSnap.ConnectProxy.Leaf.PrivateKeyPEM,
|
InlineString: leaf.PrivateKeyPEM,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -263,6 +263,16 @@ func TestListenersFromSnapshot(t *testing.T) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "ingress-gateway",
|
||||||
|
create: proxycfg.TestConfigSnapshotIngressGateway,
|
||||||
|
setup: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ingress-gateway-no-services",
|
||||||
|
create: proxycfg.TestConfigSnapshotIngressGatewayNoServices,
|
||||||
|
setup: nil,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@ -275,13 +285,7 @@ func TestListenersFromSnapshot(t *testing.T) {
|
||||||
// We need to replace the TLS certs with deterministic ones to make golden
|
// We need to replace the TLS certs with deterministic ones to make golden
|
||||||
// files workable. Note we don't update these otherwise they'd change
|
// files workable. Note we don't update these otherwise they'd change
|
||||||
// golder files for every test case and so not be any use!
|
// golder files for every test case and so not be any use!
|
||||||
if snap.ConnectProxy.Leaf != nil {
|
setupTLSRootsAndLeaf(t, snap)
|
||||||
snap.ConnectProxy.Leaf.CertPEM = golden(t, "test-leaf-cert", "")
|
|
||||||
snap.ConnectProxy.Leaf.PrivateKeyPEM = golden(t, "test-leaf-key", "")
|
|
||||||
}
|
|
||||||
if snap.Roots != nil {
|
|
||||||
snap.Roots.Roots[0].RootCert = golden(t, "test-root-cert", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.setup != nil {
|
if tt.setup != nil {
|
||||||
tt.setup(snap)
|
tt.setup(snap)
|
||||||
|
|
|
@ -335,13 +335,7 @@ func TestRoutesFromSnapshot(t *testing.T) {
|
||||||
// We need to replace the TLS certs with deterministic ones to make golden
|
// We need to replace the TLS certs with deterministic ones to make golden
|
||||||
// files workable. Note we don't update these otherwise they'd change
|
// files workable. Note we don't update these otherwise they'd change
|
||||||
// golden files for every test case and so not be any use!
|
// golden files for every test case and so not be any use!
|
||||||
if snap.ConnectProxy.Leaf != nil {
|
setupTLSRootsAndLeaf(t, snap)
|
||||||
snap.ConnectProxy.Leaf.CertPEM = golden(t, "test-leaf-cert", "")
|
|
||||||
snap.ConnectProxy.Leaf.PrivateKeyPEM = golden(t, "test-leaf-key", "")
|
|
||||||
}
|
|
||||||
if snap.Roots != nil {
|
|
||||||
snap.Roots.Roots[0].RootCert = golden(t, "test-root-cert", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.setup != nil {
|
if tt.setup != nil {
|
||||||
tt.setup(snap)
|
tt.setup(snap)
|
||||||
|
|
|
@ -267,6 +267,11 @@ func (s *Server) process(stream ADSStream, reqCh <-chan *envoy.DiscoveryRequest)
|
||||||
if rule != nil && rule.ServiceWrite(cfgSnap.Service, &authzContext) != acl.Allow {
|
if rule != nil && rule.ServiceWrite(cfgSnap.Service, &authzContext) != acl.Allow {
|
||||||
return status.Errorf(codes.PermissionDenied, "permission denied")
|
return status.Errorf(codes.PermissionDenied, "permission denied")
|
||||||
}
|
}
|
||||||
|
case structs.ServiceKindIngressGateway:
|
||||||
|
cfgSnap.ProxyID.EnterpriseMeta.FillAuthzContext(&authzContext)
|
||||||
|
if rule != nil && rule.ServiceWrite(cfgSnap.Service, &authzContext) != acl.Allow {
|
||||||
|
return status.Errorf(codes.PermissionDenied, "permission denied")
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return status.Errorf(codes.Internal, "Invalid service kind")
|
return status.Errorf(codes.Internal, "Invalid service kind")
|
||||||
}
|
}
|
||||||
|
|
|
@ -344,10 +344,10 @@ func expectedTLSContextJSON(t *testing.T, snap *proxycfg.ConfigSnapshot, require
|
||||||
"tlsCertificates": [
|
"tlsCertificates": [
|
||||||
{
|
{
|
||||||
"certificateChain": {
|
"certificateChain": {
|
||||||
"inlineString": "` + strings.Replace(snap.ConnectProxy.Leaf.CertPEM, "\n", "\\n", -1) + `"
|
"inlineString": "` + strings.Replace(snap.Leaf().CertPEM, "\n", "\\n", -1) + `"
|
||||||
},
|
},
|
||||||
"privateKey": {
|
"privateKey": {
|
||||||
"inlineString": "` + strings.Replace(snap.ConnectProxy.Leaf.PrivateKeyPEM, "\n", "\\n", -1) + `"
|
"inlineString": "` + strings.Replace(snap.Leaf().PrivateKeyPEM, "\n", "\\n", -1) + `"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -400,6 +400,7 @@ func TestServer_StreamAggregatedResources_ACLEnforcement(t *testing.T) {
|
||||||
acl string
|
acl string
|
||||||
token string
|
token string
|
||||||
wantDenied bool
|
wantDenied bool
|
||||||
|
cfgSnap *proxycfg.ConfigSnapshot
|
||||||
}{
|
}{
|
||||||
// Note that although we've stubbed actual ACL checks in the testManager
|
// Note that although we've stubbed actual ACL checks in the testManager
|
||||||
// ConnectAuthorize mock, by asserting against specific reason strings here
|
// ConnectAuthorize mock, by asserting against specific reason strings here
|
||||||
|
@ -437,6 +438,14 @@ func TestServer_StreamAggregatedResources_ACLEnforcement(t *testing.T) {
|
||||||
token: "service-write-on-not-web",
|
token: "service-write-on-not-web",
|
||||||
wantDenied: true,
|
wantDenied: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "ingress default deny, write token on different service",
|
||||||
|
defaultDeny: true,
|
||||||
|
acl: `service "not-ingress" { policy = "write" }`,
|
||||||
|
token: "service-write-on-not-ingress",
|
||||||
|
wantDenied: true,
|
||||||
|
cfgSnap: proxycfg.TestConfigSnapshotIngressGateway(t),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@ -480,7 +489,10 @@ func TestServer_StreamAggregatedResources_ACLEnforcement(t *testing.T) {
|
||||||
mgr.RegisterProxy(t, sid)
|
mgr.RegisterProxy(t, sid)
|
||||||
|
|
||||||
// Deliver a new snapshot
|
// Deliver a new snapshot
|
||||||
snap := proxycfg.TestConfigSnapshot(t)
|
snap := tt.cfgSnap
|
||||||
|
if snap == nil {
|
||||||
|
snap = proxycfg.TestConfigSnapshot(t)
|
||||||
|
}
|
||||||
mgr.DeliverConfig(t, sid, snap)
|
mgr.DeliverConfig(t, sid, snap)
|
||||||
|
|
||||||
// Send initial listener discover, in real life Envoy always sends cluster
|
// Send initial listener discover, in real life Envoy always sends cluster
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"versionInfo": "00000001",
|
||||||
|
"resources": [
|
||||||
|
],
|
||||||
|
"typeUrl": "type.googleapis.com/envoy.api.v2.Cluster",
|
||||||
|
"nonce": "00000001"
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
{
|
||||||
|
"versionInfo": "00000001",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"@type": "type.googleapis.com/envoy.api.v2.Cluster",
|
||||||
|
"name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
|
||||||
|
"altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
|
||||||
|
"type": "EDS",
|
||||||
|
"edsClusterConfig": {
|
||||||
|
"edsConfig": {
|
||||||
|
"ads": {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"connectTimeout": "5s",
|
||||||
|
"circuitBreakers": {
|
||||||
|
|
||||||
|
},
|
||||||
|
"tlsContext": {
|
||||||
|
"commonTlsContext": {
|
||||||
|
"tlsParams": {
|
||||||
|
|
||||||
|
},
|
||||||
|
"tlsCertificates": [
|
||||||
|
{
|
||||||
|
"certificateChain": {
|
||||||
|
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n"
|
||||||
|
},
|
||||||
|
"privateKey": {
|
||||||
|
"inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"validationContext": {
|
||||||
|
"trustedCa": {
|
||||||
|
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
|
||||||
|
},
|
||||||
|
"outlierDetection": {
|
||||||
|
|
||||||
|
},
|
||||||
|
"commonLbConfig": {
|
||||||
|
"healthyPanicThreshold": {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"typeUrl": "type.googleapis.com/envoy.api.v2.Cluster",
|
||||||
|
"nonce": "00000001"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"versionInfo": "00000001",
|
||||||
|
"resources": [
|
||||||
|
],
|
||||||
|
"typeUrl": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
|
||||||
|
"nonce": "00000001"
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"versionInfo": "00000001",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"@type": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
|
||||||
|
"clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"lbEndpoints": [
|
||||||
|
{
|
||||||
|
"endpoint": {
|
||||||
|
"address": {
|
||||||
|
"socketAddress": {
|
||||||
|
"address": "10.10.1.1",
|
||||||
|
"portValue": 8080
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"healthStatus": "HEALTHY",
|
||||||
|
"loadBalancingWeight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"endpoint": {
|
||||||
|
"address": {
|
||||||
|
"socketAddress": {
|
||||||
|
"address": "10.10.1.2",
|
||||||
|
"portValue": 8080
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"healthStatus": "HEALTHY",
|
||||||
|
"loadBalancingWeight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"typeUrl": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
|
||||||
|
"nonce": "00000001"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"versionInfo": "00000001",
|
||||||
|
"resources": [
|
||||||
|
],
|
||||||
|
"typeUrl": "type.googleapis.com/envoy.api.v2.Listener",
|
||||||
|
"nonce": "00000001"
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"versionInfo": "00000001",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"@type": "type.googleapis.com/envoy.api.v2.Listener",
|
||||||
|
"name": "db:2.3.4.5:9191",
|
||||||
|
"address": {
|
||||||
|
"socketAddress": {
|
||||||
|
"address": "2.3.4.5",
|
||||||
|
"portValue": 9191
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"filterChains": [
|
||||||
|
{
|
||||||
|
"filters": [
|
||||||
|
{
|
||||||
|
"name": "envoy.tcp_proxy",
|
||||||
|
"config": {
|
||||||
|
"cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
|
||||||
|
"stat_prefix": "upstream_db_tcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"typeUrl": "type.googleapis.com/envoy.api.v2.Listener",
|
||||||
|
"nonce": "00000001"
|
||||||
|
}
|
|
@ -195,23 +195,6 @@ func TestAPI_ConfigEntries_TerminatingGateway(t *testing.T) {
|
||||||
require.NotNil(t, wm)
|
require.NotNil(t, wm)
|
||||||
require.NotEqual(t, 0, wm.RequestTime)
|
require.NotEqual(t, 0, wm.RequestTime)
|
||||||
|
|
||||||
// web is associated with the other gateway, should get an error
|
|
||||||
terminating2.Services = []LinkedService{
|
|
||||||
{
|
|
||||||
Name: "*",
|
|
||||||
CAFile: "/etc/certs/ca.crt",
|
|
||||||
CertFile: "/etc/certs/client.crt",
|
|
||||||
KeyFile: "/etc/certs/tls.key",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "web",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
_, wm, err = configEntries.Set(terminating2, nil)
|
|
||||||
require.Error(t, err, "service \"web\" is associated with a different gateway")
|
|
||||||
require.Nil(t, wm)
|
|
||||||
|
|
||||||
// try again without web
|
|
||||||
terminating2.Services = []LinkedService{
|
terminating2.Services = []LinkedService{
|
||||||
{
|
{
|
||||||
Name: "*",
|
Name: "*",
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
enable_central_service_config = true
|
||||||
|
|
||||||
|
config_entries {
|
||||||
|
bootstrap {
|
||||||
|
kind = "ingress-gateway"
|
||||||
|
name = "ingress-gateway"
|
||||||
|
|
||||||
|
listeners = [
|
||||||
|
{
|
||||||
|
port = 9999
|
||||||
|
protocol = "tcp"
|
||||||
|
services = [
|
||||||
|
{
|
||||||
|
name = "s1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
services {
|
||||||
|
name = "ingress-gateway"
|
||||||
|
kind = "ingress-gateway"
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# wait for bootstrap to apply config entries
|
||||||
|
wait_for_config_entry ingress-gateway ingress-gateway
|
||||||
|
|
||||||
|
gen_envoy_bootstrap ingress-gateway 20000 primary true
|
||||||
|
gen_envoy_bootstrap s1 19000
|
||||||
|
gen_envoy_bootstrap s2 19001
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
export REQUIRED_SERVICES="$DEFAULT_REQUIRED_SERVICES ingress-gateway-primary"
|
|
@ -0,0 +1,29 @@
|
||||||
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
|
load helpers
|
||||||
|
|
||||||
|
@test "ingress proxy admin is up on :20000" {
|
||||||
|
retry_default curl -f -s localhost:20000/stats -o /dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "s1 proxy admin is up on :19000" {
|
||||||
|
retry_default curl -f -s localhost:19000/stats -o /dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "s2 proxy admin is up on :19001" {
|
||||||
|
retry_default curl -f -s localhost:19001/stats -o /dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "s1 proxy listener should be up and have right cert" {
|
||||||
|
assert_proxy_presents_cert_uri localhost:21000 s1
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "ingress-gateway should have healthy endpoints for s1" {
|
||||||
|
assert_upstream_has_endpoints_in_status 127.0.0.1:20000 s1 HEALTHY 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "ingress should be able to connect to s1 via configured port" {
|
||||||
|
run retry_default curl -s -f -d hello localhost:9999
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "$output" = "hello" ]
|
||||||
|
}
|
|
@ -563,6 +563,23 @@ services:
|
||||||
- *workdir-volume
|
- *workdir-volume
|
||||||
network_mode: service:consul-secondary
|
network_mode: service:consul-secondary
|
||||||
|
|
||||||
|
ingress-gateway-primary:
|
||||||
|
depends_on:
|
||||||
|
- consul-primary
|
||||||
|
image: "envoyproxy/envoy:v${ENVOY_VERSION}"
|
||||||
|
command:
|
||||||
|
- "envoy"
|
||||||
|
- "-c"
|
||||||
|
- "/workdir/primary/envoy/ingress-gateway-bootstrap.json"
|
||||||
|
- "-l"
|
||||||
|
- "debug"
|
||||||
|
- "--disable-hot-restart"
|
||||||
|
- "--drain-time-s"
|
||||||
|
- "1"
|
||||||
|
volumes:
|
||||||
|
- *workdir-volume
|
||||||
|
network_mode: service:consul-primary
|
||||||
|
|
||||||
verify-primary:
|
verify-primary:
|
||||||
depends_on:
|
depends_on:
|
||||||
- consul-primary
|
- consul-primary
|
||||||
|
|
|
@ -528,11 +528,11 @@ function gen_envoy_bootstrap {
|
||||||
SERVICE=$1
|
SERVICE=$1
|
||||||
ADMIN_PORT=$2
|
ADMIN_PORT=$2
|
||||||
DC=${3:-primary}
|
DC=${3:-primary}
|
||||||
IS_MGW=${4:-0}
|
IS_GW=${4:-0}
|
||||||
EXTRA_ENVOY_BS_ARGS="${5-}"
|
EXTRA_ENVOY_BS_ARGS="${5-}"
|
||||||
|
|
||||||
PROXY_ID="$SERVICE"
|
PROXY_ID="$SERVICE"
|
||||||
if ! is_set "$IS_MGW"
|
if ! is_set "$IS_GW"
|
||||||
then
|
then
|
||||||
PROXY_ID="$SERVICE-sidecar-proxy"
|
PROXY_ID="$SERVICE-sidecar-proxy"
|
||||||
fi
|
fi
|
||||||
|
|
Loading…
Reference in New Issue