mirror of https://github.com/status-im/consul.git
Update viz endpoint to include topology from intentions
This commit is contained in:
parent
932fbddd27
commit
8e74eaa684
|
@ -573,11 +573,50 @@ func registerTestCatalogEntriesMap(t *testing.T, codec rpc.ClientCodec, registra
|
|||
func registerTestTopologyEntries(t *testing.T, codec rpc.ClientCodec, token string) {
|
||||
t.Helper()
|
||||
|
||||
// api and api-proxy on node foo - upstream: web
|
||||
// ingress-gateway on node edge - upstream: api
|
||||
// api and api-proxy on node foo - transparent proxy
|
||||
// web and web-proxy on node bar - upstream: redis
|
||||
// web and web-proxy on node baz - upstream: redis
|
||||
// web and web-proxy on node baz - transparent proxy
|
||||
// redis and redis-proxy on node zip
|
||||
registrations := map[string]*structs.RegisterRequest{
|
||||
"Node edge": {
|
||||
Datacenter: "dc1",
|
||||
Node: "edge",
|
||||
ID: types.NodeID("8e3481c0-760e-4b5f-a3b8-6c8c559e8a15"),
|
||||
Address: "127.0.0.1",
|
||||
Checks: structs.HealthChecks{
|
||||
&structs.HealthCheck{
|
||||
Node: "edge",
|
||||
CheckID: "edge:alive",
|
||||
Name: "edge-liveness",
|
||||
Status: api.HealthPassing,
|
||||
},
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: token},
|
||||
},
|
||||
"Service ingress on edge": {
|
||||
Datacenter: "dc1",
|
||||
Node: "edge",
|
||||
SkipNodeUpdate: true,
|
||||
Service: &structs.NodeService{
|
||||
Kind: structs.ServiceKindIngressGateway,
|
||||
ID: "ingress",
|
||||
Service: "ingress",
|
||||
Port: 8443,
|
||||
Address: "198.18.1.1",
|
||||
},
|
||||
Checks: structs.HealthChecks{
|
||||
&structs.HealthCheck{
|
||||
Node: "edge",
|
||||
CheckID: "edge:ingress",
|
||||
Name: "ingress-liveness",
|
||||
Status: api.HealthPassing,
|
||||
ServiceID: "ingress",
|
||||
ServiceName: "ingress",
|
||||
},
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: token},
|
||||
},
|
||||
"Node foo": {
|
||||
Datacenter: "dc1",
|
||||
Node: "foo",
|
||||
|
@ -627,13 +666,8 @@ func registerTestTopologyEntries(t *testing.T, codec rpc.ClientCodec, token stri
|
|||
Port: 8443,
|
||||
Address: "198.18.1.2",
|
||||
Proxy: structs.ConnectProxyConfig{
|
||||
Mode: structs.ProxyModeTransparent,
|
||||
DestinationServiceName: "api",
|
||||
Upstreams: structs.Upstreams{
|
||||
{
|
||||
DestinationName: "web",
|
||||
LocalBindPort: 8080,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Checks: structs.HealthChecks{
|
||||
|
@ -767,13 +801,8 @@ func registerTestTopologyEntries(t *testing.T, codec rpc.ClientCodec, token stri
|
|||
Port: 8443,
|
||||
Address: "198.18.1.40",
|
||||
Proxy: structs.ConnectProxyConfig{
|
||||
Mode: structs.ProxyModeTransparent,
|
||||
DestinationServiceName: "web",
|
||||
Upstreams: structs.Upstreams{
|
||||
{
|
||||
DestinationName: "redis",
|
||||
LocalBindPort: 123,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Checks: structs.HealthChecks{
|
||||
|
@ -855,7 +884,10 @@ func registerTestTopologyEntries(t *testing.T, codec rpc.ClientCodec, token stri
|
|||
}
|
||||
registerTestCatalogEntriesMap(t, codec, registrations)
|
||||
|
||||
// Add intentions: deny all, web -> redis with L7 perms, but omit intention for api -> web
|
||||
// ingress -> api gateway config entry (but no intention)
|
||||
// wildcard deny intention
|
||||
// api -> web exact intention
|
||||
// web -> redis exact intention
|
||||
entries := []structs.ConfigEntryRequest{
|
||||
{
|
||||
Datacenter: "dc1",
|
||||
|
@ -868,6 +900,39 @@ func registerTestTopologyEntries(t *testing.T, codec rpc.ClientCodec, token stri
|
|||
},
|
||||
WriteRequest: structs.WriteRequest{Token: token},
|
||||
},
|
||||
{
|
||||
Datacenter: "dc1",
|
||||
Entry: &structs.IngressGatewayConfigEntry{
|
||||
Kind: structs.IngressGateway,
|
||||
Name: "ingress",
|
||||
Listeners: []structs.IngressListener{
|
||||
{
|
||||
Port: 8443,
|
||||
Protocol: "http",
|
||||
Services: []structs.IngressService{
|
||||
{
|
||||
Name: "api",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: token},
|
||||
},
|
||||
{
|
||||
Datacenter: "dc1",
|
||||
Entry: &structs.ServiceIntentionsConfigEntry{
|
||||
Kind: structs.ServiceIntentions,
|
||||
Name: "web",
|
||||
Sources: []*structs.SourceIntention{
|
||||
{
|
||||
Action: structs.IntentionActionAllow,
|
||||
Name: "api",
|
||||
},
|
||||
},
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: token},
|
||||
},
|
||||
{
|
||||
Datacenter: "dc1",
|
||||
Entry: &structs.ServiceIntentionsConfigEntry{
|
||||
|
|
|
@ -1690,20 +1690,67 @@ func TestInternal_ServiceTopology(t *testing.T) {
|
|||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
// api and api-proxy on node foo - upstream: web
|
||||
// web and web-proxy on node bar - upstream: redis
|
||||
// web and web-proxy on node baz - upstream: redis
|
||||
// redis and redis-proxy on node zip
|
||||
// wildcard deny intention
|
||||
// web -> redis exact intentino
|
||||
// ingress-gateway on node edge - upstream: api
|
||||
// ingress -> api gateway config entry (but no intention)
|
||||
|
||||
// api and api-proxy on node foo - transparent proxy
|
||||
// api -> web exact intention
|
||||
|
||||
// web and web-proxy on node bar - upstream: redis
|
||||
// web and web-proxy on node baz - transparent proxy
|
||||
// web -> redis exact intention
|
||||
|
||||
// redis and redis-proxy on node zip
|
||||
|
||||
registerTestTopologyEntries(t, codec, "")
|
||||
|
||||
var (
|
||||
ingress = structs.NewServiceName("ingress", structs.DefaultEnterpriseMeta())
|
||||
api = structs.NewServiceName("api", structs.DefaultEnterpriseMeta())
|
||||
web = structs.NewServiceName("web", structs.DefaultEnterpriseMeta())
|
||||
redis = structs.NewServiceName("redis", structs.DefaultEnterpriseMeta())
|
||||
)
|
||||
|
||||
t.Run("ingress", func(t *testing.T) {
|
||||
retry.Run(t, func(r *retry.R) {
|
||||
args := structs.ServiceSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
ServiceName: "ingress",
|
||||
}
|
||||
var out structs.IndexedServiceTopology
|
||||
require.NoError(r, msgpackrpc.CallWithCodec(codec, "Internal.ServiceTopology", &args, &out))
|
||||
require.False(r, out.FilteredByACLs)
|
||||
require.Equal(r, "http", out.ServiceTopology.MetricsProtocol)
|
||||
|
||||
// foo/api, foo/api-proxy
|
||||
require.Len(r, out.ServiceTopology.Upstreams, 2)
|
||||
require.Len(r, out.ServiceTopology.Downstreams, 0)
|
||||
|
||||
expectUp := map[string]structs.IntentionDecisionSummary{
|
||||
api.String(): {
|
||||
DefaultAllow: true,
|
||||
Allowed: false,
|
||||
HasPermissions: false,
|
||||
ExternalSource: "nomad",
|
||||
|
||||
// From wildcard deny
|
||||
HasExact: false,
|
||||
},
|
||||
}
|
||||
require.Equal(r, expectUp, out.ServiceTopology.UpstreamDecisions)
|
||||
|
||||
expectUpstreamSources := map[string]string{
|
||||
api.String(): structs.TopologySourceRegistration,
|
||||
}
|
||||
require.Equal(r, expectUpstreamSources, out.ServiceTopology.UpstreamSources)
|
||||
require.Empty(r, out.ServiceTopology.DownstreamSources)
|
||||
|
||||
// The ingress gateway has an explicit upstream
|
||||
require.False(r, out.ServiceTopology.TransparentProxy)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("api", func(t *testing.T) {
|
||||
retry.Run(t, func(r *retry.R) {
|
||||
args := structs.ServiceSpecificRequest{
|
||||
|
@ -1715,12 +1762,11 @@ func TestInternal_ServiceTopology(t *testing.T) {
|
|||
require.False(r, out.FilteredByACLs)
|
||||
require.Equal(r, "http", out.ServiceTopology.MetricsProtocol)
|
||||
|
||||
// bar/web, bar/web-proxy, baz/web, baz/web-proxy
|
||||
require.Len(r, out.ServiceTopology.Upstreams, 4)
|
||||
require.Len(r, out.ServiceTopology.Downstreams, 0)
|
||||
// edge/ingress
|
||||
require.Len(r, out.ServiceTopology.Downstreams, 1)
|
||||
|
||||
expectUp := map[string]structs.IntentionDecisionSummary{
|
||||
web.String(): {
|
||||
expectDown := map[string]structs.IntentionDecisionSummary{
|
||||
ingress.String(): {
|
||||
DefaultAllow: true,
|
||||
Allowed: false,
|
||||
HasPermissions: false,
|
||||
|
@ -1730,7 +1776,33 @@ func TestInternal_ServiceTopology(t *testing.T) {
|
|||
HasExact: false,
|
||||
},
|
||||
}
|
||||
require.Equal(r, expectDown, out.ServiceTopology.DownstreamDecisions)
|
||||
|
||||
expectDownstreamSources := map[string]string{
|
||||
ingress.String(): structs.TopologySourceRegistration,
|
||||
}
|
||||
require.Equal(r, expectDownstreamSources, out.ServiceTopology.DownstreamSources)
|
||||
|
||||
// bar/web, bar/web-proxy, baz/web, baz/web-proxy
|
||||
require.Len(r, out.ServiceTopology.Upstreams, 4)
|
||||
|
||||
expectUp := map[string]structs.IntentionDecisionSummary{
|
||||
web.String(): {
|
||||
DefaultAllow: true,
|
||||
Allowed: true,
|
||||
HasPermissions: false,
|
||||
HasExact: true,
|
||||
},
|
||||
}
|
||||
require.Equal(r, expectUp, out.ServiceTopology.UpstreamDecisions)
|
||||
|
||||
expectUpstreamSources := map[string]string{
|
||||
web.String(): structs.TopologySourceSpecificIntention,
|
||||
}
|
||||
require.Equal(r, expectUpstreamSources, out.ServiceTopology.UpstreamSources)
|
||||
|
||||
// The only instance of api's proxy is in transparent mode
|
||||
require.True(r, out.ServiceTopology.TransparentProxy)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -1751,16 +1823,18 @@ func TestInternal_ServiceTopology(t *testing.T) {
|
|||
expectDown := map[string]structs.IntentionDecisionSummary{
|
||||
api.String(): {
|
||||
DefaultAllow: true,
|
||||
Allowed: false,
|
||||
Allowed: true,
|
||||
HasPermissions: false,
|
||||
ExternalSource: "nomad",
|
||||
|
||||
// From wildcard deny
|
||||
HasExact: false,
|
||||
HasExact: true,
|
||||
},
|
||||
}
|
||||
require.Equal(r, expectDown, out.ServiceTopology.DownstreamDecisions)
|
||||
|
||||
expectDownstreamSources := map[string]string{
|
||||
api.String(): structs.TopologySourceSpecificIntention,
|
||||
}
|
||||
require.Equal(r, expectDownstreamSources, out.ServiceTopology.DownstreamSources)
|
||||
|
||||
// zip/redis, zip/redis-proxy
|
||||
require.Len(r, out.ServiceTopology.Upstreams, 2)
|
||||
|
||||
|
@ -1773,6 +1847,15 @@ func TestInternal_ServiceTopology(t *testing.T) {
|
|||
},
|
||||
}
|
||||
require.Equal(r, expectUp, out.ServiceTopology.UpstreamDecisions)
|
||||
|
||||
expectUpstreamSources := map[string]string{
|
||||
// We prefer from-registration over intention source when there is a mix
|
||||
redis.String(): structs.TopologySourceRegistration,
|
||||
}
|
||||
require.Equal(r, expectUpstreamSources, out.ServiceTopology.UpstreamSources)
|
||||
|
||||
// Not all instances of web are in transparent mode
|
||||
require.False(r, out.ServiceTopology.TransparentProxy)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -1801,6 +1884,15 @@ func TestInternal_ServiceTopology(t *testing.T) {
|
|||
},
|
||||
}
|
||||
require.Equal(r, expectDown, out.ServiceTopology.DownstreamDecisions)
|
||||
|
||||
expectDownstreamSources := map[string]string{
|
||||
web.String(): structs.TopologySourceRegistration,
|
||||
}
|
||||
require.Equal(r, expectDownstreamSources, out.ServiceTopology.DownstreamSources)
|
||||
require.Empty(r, out.ServiceTopology.UpstreamSources)
|
||||
|
||||
// No proxies are in transparent mode
|
||||
require.False(r, out.ServiceTopology.TransparentProxy)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -1825,9 +1917,17 @@ func TestInternal_ServiceTopology_ACL(t *testing.T) {
|
|||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
// api and api-proxy on node foo - upstream: web
|
||||
// wildcard deny intention
|
||||
// ingress-gateway on node edge - upstream: api
|
||||
// ingress -> api gateway config entry (but no intention)
|
||||
|
||||
// api and api-proxy on node foo - transparent proxy
|
||||
// api -> web exact intention
|
||||
|
||||
// web and web-proxy on node bar - upstream: redis
|
||||
// web and web-proxy on node baz - upstream: redis
|
||||
// web and web-proxy on node baz - transparent proxy
|
||||
// web -> redis exact intention
|
||||
|
||||
// redis and redis-proxy on node zip
|
||||
registerTestTopologyEntries(t, codec, TestDefaultMasterToken)
|
||||
|
||||
|
|
|
@ -2853,6 +2853,8 @@ func checkProtocolMatch(tx ReadTxn, ws memdb.WatchSet, svc *structs.GatewayServi
|
|||
return idx, svc.Protocol == protocol, nil
|
||||
}
|
||||
|
||||
// TODO(freddy) Split this up. The upstream/downstream logic is very similar.
|
||||
// TODO(freddy) Add comprehensive state store test
|
||||
func (s *Store) ServiceTopology(
|
||||
ws memdb.WatchSet,
|
||||
dc, service string,
|
||||
|
@ -2863,14 +2865,15 @@ func (s *Store) ServiceTopology(
|
|||
tx := s.db.ReadTxn()
|
||||
defer tx.Abort()
|
||||
|
||||
sn := structs.NewServiceName(service, entMeta)
|
||||
|
||||
var (
|
||||
maxIdx uint64
|
||||
protocol string
|
||||
err error
|
||||
|
||||
sn = structs.NewServiceName(service, entMeta)
|
||||
fullyTransparent bool
|
||||
hasTransparent bool
|
||||
)
|
||||
|
||||
switch kind {
|
||||
case structs.ServiceKindIngressGateway:
|
||||
maxIdx, protocol, err = metricsProtocolForIngressGateway(tx, ws, sn)
|
||||
|
@ -2884,6 +2887,38 @@ func (s *Store) ServiceTopology(
|
|||
return 0, nil, fmt.Errorf("failed to fetch protocol for service %s: %v", sn.String(), err)
|
||||
}
|
||||
|
||||
// Fetch connect endpoints for the target service in order to learn if its proxies are configured as
|
||||
// transparent proxies.
|
||||
if entMeta == nil {
|
||||
entMeta = structs.DefaultEnterpriseMeta()
|
||||
}
|
||||
q := Query{Value: service, EnterpriseMeta: *entMeta}
|
||||
|
||||
idx, proxies, err := serviceNodesTxn(tx, ws, indexConnect, q)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("failed to fetch connect endpoints for service %s: %v", sn.String(), err)
|
||||
}
|
||||
if idx > maxIdx {
|
||||
maxIdx = idx
|
||||
}
|
||||
if len(proxies) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
fullyTransparent = true
|
||||
for _, proxy := range proxies {
|
||||
switch proxy.ServiceProxy.Mode {
|
||||
case structs.ProxyModeTransparent:
|
||||
hasTransparent = true
|
||||
|
||||
default:
|
||||
// Only consider the target proxy to be transparent when all instances are in that mode.
|
||||
// This is done because the flag is used to display warnings about proxies needing to enable
|
||||
// transparent proxy mode. If ANY instance isn't in the right mode then the warming applies.
|
||||
fullyTransparent = false
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return 0, nil, fmt.Errorf("unsupported kind %q", kind)
|
||||
}
|
||||
|
@ -2895,7 +2930,48 @@ func (s *Store) ServiceTopology(
|
|||
if idx > maxIdx {
|
||||
maxIdx = idx
|
||||
}
|
||||
idx, upstreams, err := s.combinedServiceNodesTxn(tx, ws, upstreamNames)
|
||||
|
||||
var (
|
||||
seenUpstreams = make(map[string]struct{})
|
||||
upstreamSources = make(map[string]string)
|
||||
)
|
||||
for _, un := range upstreamNames {
|
||||
if _, ok := seenUpstreams[un.String()]; !ok {
|
||||
seenUpstreams[un.String()] = struct{}{}
|
||||
}
|
||||
upstreamSources[un.String()] = structs.TopologySourceRegistration
|
||||
}
|
||||
|
||||
idx, intentionUpstreams, err := s.intentionTopologyTxn(tx, ws, sn, false, defaultAllow)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if idx > maxIdx {
|
||||
maxIdx = idx
|
||||
}
|
||||
|
||||
upstreamDecisions := make(map[string]structs.IntentionDecisionSummary)
|
||||
for _, svc := range intentionUpstreams {
|
||||
if _, ok := seenUpstreams[svc.Name.String()]; ok {
|
||||
// Avoid duplicating entry
|
||||
continue
|
||||
}
|
||||
upstreamDecisions[svc.Name.String()] = svc.Decision
|
||||
upstreamNames = append(upstreamNames, svc.Name)
|
||||
|
||||
var source string
|
||||
switch {
|
||||
case svc.Decision.HasExact:
|
||||
source = structs.TopologySourceSpecificIntention
|
||||
case svc.Decision.DefaultAllow:
|
||||
source = structs.TopologySourceDefaultAllow
|
||||
default:
|
||||
source = structs.TopologySourceWildcardIntention
|
||||
}
|
||||
upstreamSources[svc.Name.String()] = source
|
||||
}
|
||||
|
||||
idx, unfilteredUpstreams, err := s.combinedServiceNodesTxn(tx, ws, upstreamNames)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("failed to get upstreams for %q: %v", sn.String(), err)
|
||||
}
|
||||
|
@ -2903,14 +2979,32 @@ func (s *Store) ServiceTopology(
|
|||
maxIdx = idx
|
||||
}
|
||||
|
||||
upstreamDecisions := make(map[string]structs.IntentionDecisionSummary)
|
||||
var upstreams structs.CheckServiceNodes
|
||||
for _, upstream := range unfilteredUpstreams {
|
||||
sn := upstream.Service.CompoundServiceName()
|
||||
if upstream.Service.Kind == structs.ServiceKindConnectProxy {
|
||||
sn = structs.NewServiceName(upstream.Service.Proxy.DestinationServiceName, &upstream.Service.EnterpriseMeta)
|
||||
}
|
||||
|
||||
// Avoid returning upstreams from intentions when none of the proxy instances of the target are in transparent mode.
|
||||
if !hasTransparent && upstreamSources[sn.String()] != structs.TopologySourceRegistration {
|
||||
continue
|
||||
}
|
||||
upstreams = append(upstreams, upstream)
|
||||
}
|
||||
|
||||
matchEntry := structs.IntentionMatchEntry{
|
||||
Namespace: entMeta.NamespaceOrDefault(),
|
||||
Name: service,
|
||||
}
|
||||
_, srcIntentions, err := compatIntentionMatchOneTxn(
|
||||
tx,
|
||||
ws,
|
||||
matchEntry,
|
||||
|
||||
// The given service is a source relative to its upstreams
|
||||
_, srcIntentions, err := compatIntentionMatchOneTxn(tx, ws, matchEntry, structs.IntentionMatchSource)
|
||||
structs.IntentionMatchSource,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("failed to query intentions for %s", sn.String())
|
||||
}
|
||||
|
@ -2930,7 +3024,48 @@ func (s *Store) ServiceTopology(
|
|||
if idx > maxIdx {
|
||||
maxIdx = idx
|
||||
}
|
||||
idx, downstreams, err := s.combinedServiceNodesTxn(tx, ws, downstreamNames)
|
||||
|
||||
var (
|
||||
seenDownstreams = make(map[string]struct{})
|
||||
downstreamSources = make(map[string]string)
|
||||
)
|
||||
for _, dn := range downstreamNames {
|
||||
if _, ok := seenDownstreams[dn.String()]; !ok {
|
||||
seenDownstreams[dn.String()] = struct{}{}
|
||||
}
|
||||
downstreamSources[dn.String()] = structs.TopologySourceRegistration
|
||||
}
|
||||
|
||||
idx, intentionDownstreams, err := s.intentionTopologyTxn(tx, ws, sn, true, defaultAllow)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if idx > maxIdx {
|
||||
maxIdx = idx
|
||||
}
|
||||
|
||||
downstreamDecisions := make(map[string]structs.IntentionDecisionSummary)
|
||||
for _, svc := range intentionDownstreams {
|
||||
if _, ok := seenDownstreams[svc.Name.String()]; ok {
|
||||
// Avoid duplicating entry
|
||||
continue
|
||||
}
|
||||
downstreamNames = append(downstreamNames, svc.Name)
|
||||
downstreamDecisions[svc.Name.String()] = svc.Decision
|
||||
|
||||
var source string
|
||||
switch {
|
||||
case svc.Decision.HasExact:
|
||||
source = structs.TopologySourceSpecificIntention
|
||||
case svc.Decision.DefaultAllow:
|
||||
source = structs.TopologySourceDefaultAllow
|
||||
default:
|
||||
source = structs.TopologySourceWildcardIntention
|
||||
}
|
||||
downstreamSources[svc.Name.String()] = source
|
||||
}
|
||||
|
||||
idx, unfilteredDownstreams, err := s.combinedServiceNodesTxn(tx, ws, downstreamNames)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("failed to get downstreams for %q: %v", sn.String(), err)
|
||||
}
|
||||
|
@ -2938,12 +3073,39 @@ func (s *Store) ServiceTopology(
|
|||
maxIdx = idx
|
||||
}
|
||||
|
||||
// Store downstreams with at least one instance in transparent proxy mode.
|
||||
// This is to avoid returning downstreams from intentions when none of the downstreams are transparent proxies.
|
||||
tproxyMap := make(map[structs.ServiceName]struct{})
|
||||
for _, downstream := range unfilteredDownstreams {
|
||||
if downstream.Service.Proxy.Mode == structs.ProxyModeTransparent {
|
||||
sn := structs.NewServiceName(downstream.Service.Proxy.DestinationServiceName, &downstream.Service.EnterpriseMeta)
|
||||
tproxyMap[sn] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
var downstreams structs.CheckServiceNodes
|
||||
for _, downstream := range unfilteredDownstreams {
|
||||
sn := downstream.Service.CompoundServiceName()
|
||||
if downstream.Service.Kind == structs.ServiceKindConnectProxy {
|
||||
sn = structs.NewServiceName(downstream.Service.Proxy.DestinationServiceName, &downstream.Service.EnterpriseMeta)
|
||||
}
|
||||
if _, ok := tproxyMap[sn]; !ok && downstreamSources[sn.String()] != structs.TopologySourceRegistration {
|
||||
continue
|
||||
}
|
||||
downstreams = append(downstreams, downstream)
|
||||
}
|
||||
|
||||
_, dstIntentions, err := compatIntentionMatchOneTxn(
|
||||
tx,
|
||||
ws,
|
||||
matchEntry,
|
||||
|
||||
// The given service is a destination relative to its downstreams
|
||||
_, dstIntentions, err := compatIntentionMatchOneTxn(tx, ws, matchEntry, structs.IntentionMatchDestination)
|
||||
structs.IntentionMatchDestination,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("failed to query intentions for %s", sn.String())
|
||||
}
|
||||
downstreamDecisions := make(map[string]structs.IntentionDecisionSummary)
|
||||
for _, dn := range downstreamNames {
|
||||
decision, err := s.IntentionDecision(dn.Name, dn.NamespaceOrDefault(), dstIntentions, structs.IntentionMatchSource, defaultAllow, false)
|
||||
if err != nil {
|
||||
|
@ -2954,11 +3116,14 @@ func (s *Store) ServiceTopology(
|
|||
}
|
||||
|
||||
resp := &structs.ServiceTopology{
|
||||
TransparentProxy: fullyTransparent,
|
||||
MetricsProtocol: protocol,
|
||||
Upstreams: upstreams,
|
||||
Downstreams: downstreams,
|
||||
UpstreamDecisions: upstreamDecisions,
|
||||
DownstreamDecisions: downstreamDecisions,
|
||||
UpstreamSources: upstreamSources,
|
||||
DownstreamSources: downstreamSources,
|
||||
}
|
||||
return maxIdx, resp, nil
|
||||
}
|
||||
|
@ -2995,7 +3160,6 @@ func (s *Store) combinedServiceNodesTxn(tx ReadTxn, ws memdb.WatchSet, names []s
|
|||
|
||||
// downstreamsForServiceTxn will find all downstream services that could route traffic to the input service.
|
||||
// There are two factors at play. Upstreams defined in a proxy registration, and the discovery chain for those upstreams.
|
||||
// TODO (freddy): Account for ingress gateways
|
||||
func (s *Store) downstreamsForServiceTxn(tx ReadTxn, ws memdb.WatchSet, dc string, service structs.ServiceName) (uint64, []structs.ServiceName, error) {
|
||||
// First fetch services that have discovery chains that eventually route to the target service
|
||||
idx, sources, err := s.discoveryChainSourcesTxn(tx, ws, dc, service)
|
||||
|
|
|
@ -933,6 +933,11 @@ func intentionMatchGetParams(entry structs.IntentionMatchEntry) ([][]interface{}
|
|||
return result, nil
|
||||
}
|
||||
|
||||
type ServiceWithDecision struct {
|
||||
Name structs.ServiceName
|
||||
Decision structs.IntentionDecisionSummary
|
||||
}
|
||||
|
||||
// IntentionTopology returns the upstreams or downstreams of a service. Upstreams and downstreams are inferred from
|
||||
// intentions. If intentions allow a connection from the target to some candidate service, the candidate service is considered
|
||||
// an upstream of the target.
|
||||
|
@ -941,6 +946,25 @@ func (s *Store) IntentionTopology(ws memdb.WatchSet,
|
|||
tx := s.db.ReadTxn()
|
||||
defer tx.Abort()
|
||||
|
||||
idx, services, err := s.intentionTopologyTxn(tx, ws, target, downstreams, defaultDecision)
|
||||
if err != nil {
|
||||
requested := "upstreams"
|
||||
if downstreams {
|
||||
requested = "downstreams"
|
||||
}
|
||||
return 0, nil, fmt.Errorf("failed to fetch %s for %s: %v", requested, target.String(), err)
|
||||
}
|
||||
|
||||
var resp structs.ServiceList
|
||||
for _, svc := range services {
|
||||
resp = append(resp, svc.Name)
|
||||
}
|
||||
return idx, resp, nil
|
||||
}
|
||||
|
||||
func (s *Store) intentionTopologyTxn(tx ReadTxn, ws memdb.WatchSet,
|
||||
target structs.ServiceName, downstreams bool, defaultDecision acl.EnforcementDecision) (uint64, []ServiceWithDecision, error) {
|
||||
|
||||
var maxIdx uint64
|
||||
|
||||
// If querying the upstreams for a service, we first query intentions that apply to the target service as a source.
|
||||
|
@ -999,7 +1023,7 @@ func (s *Store) IntentionTopology(ws memdb.WatchSet,
|
|||
if downstreams {
|
||||
decisionMatchType = structs.IntentionMatchSource
|
||||
}
|
||||
result := make(structs.ServiceList, 0, len(allServices))
|
||||
result := make([]ServiceWithDecision, 0, len(allServices))
|
||||
for _, candidate := range allServices {
|
||||
if candidate.Name == structs.ConsulServiceName {
|
||||
continue
|
||||
|
@ -1016,7 +1040,11 @@ func (s *Store) IntentionTopology(ws memdb.WatchSet,
|
|||
if !decision.Allowed || target.Matches(candidate) {
|
||||
continue
|
||||
}
|
||||
result = append(result, candidate)
|
||||
|
||||
result = append(result, ServiceWithDecision{
|
||||
Name: candidate,
|
||||
Decision: decision,
|
||||
})
|
||||
}
|
||||
return maxIdx, result, err
|
||||
}
|
||||
|
|
|
@ -35,6 +35,22 @@ const (
|
|||
MeshGatewayModeRemote MeshGatewayMode = "remote"
|
||||
)
|
||||
|
||||
const (
|
||||
// TODO (freddy) Should we have a TopologySourceMixed when there is a mix of proxy reg and tproxy?
|
||||
// Currently we label as proxy-registration if ANY instance has the explicit upstream definition.
|
||||
// TopologySourceRegistration is used to label upstreams or downstreams from explicit upstream definitions
|
||||
TopologySourceRegistration = "proxy-registration"
|
||||
|
||||
// TopologySourceSpecificIntention is used to label upstreams or downstreams from specific intentions
|
||||
TopologySourceSpecificIntention = "specific-intention"
|
||||
|
||||
// TopologySourceWildcardIntention is used to label upstreams or downstreams from wildcard intentions
|
||||
TopologySourceWildcardIntention = "wildcard-intention"
|
||||
|
||||
// TopologySourceDefaultAllow is used to label upstreams or downstreams from default allow ACL policy
|
||||
TopologySourceDefaultAllow = "default-allow"
|
||||
)
|
||||
|
||||
// MeshGatewayConfig controls how Mesh Gateways are configured and used
|
||||
// This is a struct to allow for future additions without having more free-hanging
|
||||
// configuration items all over the place
|
||||
|
|
|
@ -1924,6 +1924,17 @@ type ServiceTopology struct {
|
|||
|
||||
// MetricsProtocol is the protocol of the service being queried
|
||||
MetricsProtocol string
|
||||
|
||||
// TransparentProxy describes whether all instances of the proxy
|
||||
// service are in transparent mode.
|
||||
TransparentProxy bool
|
||||
|
||||
// (Up|Down)streamSources are maps with labels for why each service is being
|
||||
// returned. Services can be upstreams or downstreams due to
|
||||
// explicit upstream definition or various types of intention policies:
|
||||
// specific, wildcard, or default allow.
|
||||
UpstreamSources map[string]string
|
||||
DownstreamSources map[string]string
|
||||
}
|
||||
|
||||
// IndexedConfigEntries has its own encoding logic which differs from
|
||||
|
|
|
@ -32,6 +32,8 @@ type ServiceSummary struct {
|
|||
ChecksWarning int
|
||||
ChecksCritical int
|
||||
GatewayConfig GatewayConfig
|
||||
TransparentProxy bool
|
||||
transparentProxySet bool
|
||||
|
||||
structs.EnterpriseMeta
|
||||
}
|
||||
|
@ -61,11 +63,13 @@ type ServiceListingSummary struct {
|
|||
type ServiceTopologySummary struct {
|
||||
ServiceSummary
|
||||
|
||||
Source string
|
||||
Intention structs.IntentionDecisionSummary
|
||||
}
|
||||
|
||||
type ServiceTopology struct {
|
||||
Protocol string
|
||||
TransparentProxy bool
|
||||
Upstreams []*ServiceTopologySummary
|
||||
Downstreams []*ServiceTopologySummary
|
||||
FilteredByACLs bool
|
||||
|
@ -334,6 +338,7 @@ RPC:
|
|||
sum := ServiceTopologySummary{
|
||||
ServiceSummary: *svc,
|
||||
Intention: out.ServiceTopology.UpstreamDecisions[sn.String()],
|
||||
Source: out.ServiceTopology.UpstreamSources[sn.String()],
|
||||
}
|
||||
upstreamResp = append(upstreamResp, &sum)
|
||||
}
|
||||
|
@ -344,11 +349,13 @@ RPC:
|
|||
sum := ServiceTopologySummary{
|
||||
ServiceSummary: *svc,
|
||||
Intention: out.ServiceTopology.DownstreamDecisions[sn.String()],
|
||||
Source: out.ServiceTopology.DownstreamSources[sn.String()],
|
||||
}
|
||||
downstreamResp = append(downstreamResp, &sum)
|
||||
}
|
||||
|
||||
topo := ServiceTopology{
|
||||
TransparentProxy: out.ServiceTopology.TransparentProxy,
|
||||
Protocol: out.ServiceTopology.MetricsProtocol,
|
||||
Upstreams: upstreamResp,
|
||||
Downstreams: downstreamResp,
|
||||
|
@ -410,6 +417,17 @@ func summarizeServices(dump structs.ServiceDump, cfg *config.RuntimeConfig, dc s
|
|||
}
|
||||
destination.checks[uid] = check
|
||||
}
|
||||
|
||||
// Only consider the target service to be transparent when all its proxy instances are in that mode.
|
||||
// This is done because the flag is used to display warnings about proxies needing to enable
|
||||
// transparent proxy mode. If ANY instance isn't in the right mode then the warming applies.
|
||||
if svc.Proxy.Mode == structs.ProxyModeTransparent && !destination.transparentProxySet {
|
||||
destination.TransparentProxy = true
|
||||
}
|
||||
if svc.Proxy.Mode != structs.ProxyModeTransparent {
|
||||
destination.TransparentProxy = false
|
||||
}
|
||||
destination.transparentProxySet = true
|
||||
}
|
||||
for _, tag := range svc.Tags {
|
||||
found := false
|
||||
|
|
|
@ -396,6 +396,7 @@ func TestUiServices(t *testing.T) {
|
|||
|
||||
// internal accounting that users don't see can be blown away
|
||||
for _, sum := range summary {
|
||||
sum.transparentProxySet = false
|
||||
sum.externalSourceSet = nil
|
||||
sum.checks = nil
|
||||
}
|
||||
|
@ -1078,12 +1079,7 @@ func TestUIServiceTopology(t *testing.T) {
|
|||
Address: "198.18.1.2",
|
||||
Proxy: structs.ConnectProxyConfig{
|
||||
DestinationServiceName: "api",
|
||||
Upstreams: structs.Upstreams{
|
||||
{
|
||||
DestinationName: "web",
|
||||
LocalBindPort: 8080,
|
||||
},
|
||||
},
|
||||
Mode: structs.ProxyModeTransparent,
|
||||
},
|
||||
},
|
||||
Checks: structs.HealthChecks{
|
||||
|
@ -1210,12 +1206,7 @@ func TestUIServiceTopology(t *testing.T) {
|
|||
Address: "198.18.1.40",
|
||||
Proxy: structs.ConnectProxyConfig{
|
||||
DestinationServiceName: "web",
|
||||
Upstreams: structs.Upstreams{
|
||||
{
|
||||
DestinationName: "redis",
|
||||
LocalBindPort: 123,
|
||||
},
|
||||
},
|
||||
Mode: structs.ProxyModeTransparent,
|
||||
},
|
||||
},
|
||||
Checks: structs.HealthChecks{
|
||||
|
@ -1296,8 +1287,10 @@ func TestUIServiceTopology(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Add intentions: deny all, ingress -> api, web -> redis with L7 perms, but omit intention for api -> web
|
||||
// Add ingress config: ingress -> api
|
||||
// ingress -> api gateway config entry (but no intention)
|
||||
// wildcard deny intention
|
||||
// api -> web exact intention
|
||||
// web -> redis exact intention
|
||||
{
|
||||
entries := []structs.ConfigEntryRequest{
|
||||
{
|
||||
|
@ -1318,6 +1311,20 @@ func TestUIServiceTopology(t *testing.T) {
|
|||
Protocol: "tcp",
|
||||
},
|
||||
},
|
||||
{
|
||||
Datacenter: "dc1",
|
||||
Entry: &structs.ServiceIntentionsConfigEntry{
|
||||
Kind: structs.ServiceIntentions,
|
||||
Name: "*",
|
||||
Meta: map[string]string{structs.MetaExternalSource: "nomad"},
|
||||
Sources: []*structs.SourceIntention{
|
||||
{
|
||||
Name: "*",
|
||||
Action: structs.IntentionActionDeny,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Datacenter: "dc1",
|
||||
Entry: &structs.ServiceIntentionsConfigEntry{
|
||||
|
@ -1342,12 +1349,11 @@ func TestUIServiceTopology(t *testing.T) {
|
|||
Datacenter: "dc1",
|
||||
Entry: &structs.ServiceIntentionsConfigEntry{
|
||||
Kind: structs.ServiceIntentions,
|
||||
Name: "*",
|
||||
Meta: map[string]string{structs.MetaExternalSource: "nomad"},
|
||||
Name: "web",
|
||||
Sources: []*structs.SourceIntention{
|
||||
{
|
||||
Name: "*",
|
||||
Action: structs.IntentionActionDeny,
|
||||
Action: structs.IntentionActionAllow,
|
||||
Name: "api",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1420,6 +1426,7 @@ func TestUIServiceTopology(t *testing.T) {
|
|||
|
||||
expect := ServiceTopology{
|
||||
Protocol: "tcp",
|
||||
TransparentProxy: false,
|
||||
Upstreams: []*ServiceTopologySummary{
|
||||
{
|
||||
ServiceSummary: ServiceSummary{
|
||||
|
@ -1429,6 +1436,7 @@ func TestUIServiceTopology(t *testing.T) {
|
|||
InstanceCount: 1,
|
||||
ChecksPassing: 3,
|
||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||
TransparentProxy: true,
|
||||
},
|
||||
Intention: structs.IntentionDecisionSummary{
|
||||
DefaultAllow: true,
|
||||
|
@ -1436,6 +1444,7 @@ func TestUIServiceTopology(t *testing.T) {
|
|||
HasPermissions: false,
|
||||
HasExact: true,
|
||||
},
|
||||
Source: structs.TopologySourceRegistration,
|
||||
},
|
||||
},
|
||||
Downstreams: []*ServiceTopologySummary{},
|
||||
|
@ -1447,6 +1456,7 @@ func TestUIServiceTopology(t *testing.T) {
|
|||
for _, u := range result.Upstreams {
|
||||
u.externalSourceSet = nil
|
||||
u.checks = nil
|
||||
u.transparentProxySet = false
|
||||
}
|
||||
require.Equal(r, expect, result)
|
||||
})
|
||||
|
@ -1463,6 +1473,7 @@ func TestUIServiceTopology(t *testing.T) {
|
|||
|
||||
expect := ServiceTopology{
|
||||
Protocol: "tcp",
|
||||
TransparentProxy: true,
|
||||
Downstreams: []*ServiceTopologySummary{
|
||||
{
|
||||
ServiceSummary: ServiceSummary{
|
||||
|
@ -1473,6 +1484,7 @@ func TestUIServiceTopology(t *testing.T) {
|
|||
InstanceCount: 1,
|
||||
ChecksPassing: 1,
|
||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||
TransparentProxy: false,
|
||||
},
|
||||
Intention: structs.IntentionDecisionSummary{
|
||||
DefaultAllow: true,
|
||||
|
@ -1480,6 +1492,7 @@ func TestUIServiceTopology(t *testing.T) {
|
|||
HasPermissions: false,
|
||||
HasExact: true,
|
||||
},
|
||||
Source: structs.TopologySourceRegistration,
|
||||
},
|
||||
},
|
||||
Upstreams: []*ServiceTopologySummary{
|
||||
|
@ -1493,16 +1506,15 @@ func TestUIServiceTopology(t *testing.T) {
|
|||
ChecksWarning: 1,
|
||||
ChecksCritical: 2,
|
||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||
TransparentProxy: false,
|
||||
},
|
||||
Intention: structs.IntentionDecisionSummary{
|
||||
DefaultAllow: true,
|
||||
Allowed: false,
|
||||
Allowed: true,
|
||||
HasPermissions: false,
|
||||
ExternalSource: "nomad",
|
||||
|
||||
// From wildcard deny
|
||||
HasExact: false,
|
||||
HasExact: true,
|
||||
},
|
||||
Source: structs.TopologySourceSpecificIntention,
|
||||
},
|
||||
},
|
||||
FilteredByACLs: false,
|
||||
|
@ -1511,10 +1523,12 @@ func TestUIServiceTopology(t *testing.T) {
|
|||
|
||||
// Internal accounting that is not returned in JSON response
|
||||
for _, u := range result.Upstreams {
|
||||
u.transparentProxySet = false
|
||||
u.externalSourceSet = nil
|
||||
u.checks = nil
|
||||
}
|
||||
for _, d := range result.Downstreams {
|
||||
d.transparentProxySet = false
|
||||
d.externalSourceSet = nil
|
||||
d.checks = nil
|
||||
}
|
||||
|
@ -1533,6 +1547,7 @@ func TestUIServiceTopology(t *testing.T) {
|
|||
|
||||
expect := ServiceTopology{
|
||||
Protocol: "http",
|
||||
TransparentProxy: false,
|
||||
Upstreams: []*ServiceTopologySummary{
|
||||
{
|
||||
ServiceSummary: ServiceSummary{
|
||||
|
@ -1543,6 +1558,7 @@ func TestUIServiceTopology(t *testing.T) {
|
|||
ChecksPassing: 2,
|
||||
ChecksCritical: 1,
|
||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||
TransparentProxy: false,
|
||||
},
|
||||
Intention: structs.IntentionDecisionSummary{
|
||||
DefaultAllow: true,
|
||||
|
@ -1550,6 +1566,7 @@ func TestUIServiceTopology(t *testing.T) {
|
|||
HasPermissions: true,
|
||||
HasExact: true,
|
||||
},
|
||||
Source: structs.TopologySourceRegistration,
|
||||
},
|
||||
},
|
||||
Downstreams: []*ServiceTopologySummary{
|
||||
|
@ -1561,16 +1578,15 @@ func TestUIServiceTopology(t *testing.T) {
|
|||
InstanceCount: 1,
|
||||
ChecksPassing: 3,
|
||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||
TransparentProxy: true,
|
||||
},
|
||||
Intention: structs.IntentionDecisionSummary{
|
||||
DefaultAllow: true,
|
||||
Allowed: false,
|
||||
Allowed: true,
|
||||
HasPermissions: false,
|
||||
ExternalSource: "nomad",
|
||||
|
||||
// From wildcard deny
|
||||
HasExact: false,
|
||||
HasExact: true,
|
||||
},
|
||||
Source: structs.TopologySourceSpecificIntention,
|
||||
},
|
||||
},
|
||||
FilteredByACLs: false,
|
||||
|
@ -1579,10 +1595,12 @@ func TestUIServiceTopology(t *testing.T) {
|
|||
|
||||
// Internal accounting that is not returned in JSON response
|
||||
for _, u := range result.Upstreams {
|
||||
u.transparentProxySet = false
|
||||
u.externalSourceSet = nil
|
||||
u.checks = nil
|
||||
}
|
||||
for _, d := range result.Downstreams {
|
||||
d.transparentProxySet = false
|
||||
d.externalSourceSet = nil
|
||||
d.checks = nil
|
||||
}
|
||||
|
@ -1620,6 +1638,7 @@ func TestUIServiceTopology(t *testing.T) {
|
|||
HasPermissions: true,
|
||||
HasExact: true,
|
||||
},
|
||||
Source: structs.TopologySourceRegistration,
|
||||
},
|
||||
},
|
||||
FilteredByACLs: false,
|
||||
|
@ -1628,6 +1647,7 @@ func TestUIServiceTopology(t *testing.T) {
|
|||
|
||||
// Internal accounting that is not returned in JSON response
|
||||
for _, d := range result.Downstreams {
|
||||
d.transparentProxySet = false
|
||||
d.externalSourceSet = nil
|
||||
d.checks = nil
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue