Update viz endpoint to include topology from intentions

This commit is contained in:
freddygv 2021-04-13 10:12:13 -06:00
parent 932fbddd27
commit 8e74eaa684
8 changed files with 560 additions and 138 deletions

View File

@ -573,11 +573,50 @@ func registerTestCatalogEntriesMap(t *testing.T, codec rpc.ClientCodec, registra
func registerTestTopologyEntries(t *testing.T, codec rpc.ClientCodec, token string) { func registerTestTopologyEntries(t *testing.T, codec rpc.ClientCodec, token string) {
t.Helper() 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 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 // redis and redis-proxy on node zip
registrations := map[string]*structs.RegisterRequest{ 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": { "Node foo": {
Datacenter: "dc1", Datacenter: "dc1",
Node: "foo", Node: "foo",
@ -627,13 +666,8 @@ func registerTestTopologyEntries(t *testing.T, codec rpc.ClientCodec, token stri
Port: 8443, Port: 8443,
Address: "198.18.1.2", Address: "198.18.1.2",
Proxy: structs.ConnectProxyConfig{ Proxy: structs.ConnectProxyConfig{
Mode: structs.ProxyModeTransparent,
DestinationServiceName: "api", DestinationServiceName: "api",
Upstreams: structs.Upstreams{
{
DestinationName: "web",
LocalBindPort: 8080,
},
},
}, },
}, },
Checks: structs.HealthChecks{ Checks: structs.HealthChecks{
@ -767,13 +801,8 @@ func registerTestTopologyEntries(t *testing.T, codec rpc.ClientCodec, token stri
Port: 8443, Port: 8443,
Address: "198.18.1.40", Address: "198.18.1.40",
Proxy: structs.ConnectProxyConfig{ Proxy: structs.ConnectProxyConfig{
Mode: structs.ProxyModeTransparent,
DestinationServiceName: "web", DestinationServiceName: "web",
Upstreams: structs.Upstreams{
{
DestinationName: "redis",
LocalBindPort: 123,
},
},
}, },
}, },
Checks: structs.HealthChecks{ Checks: structs.HealthChecks{
@ -855,7 +884,10 @@ func registerTestTopologyEntries(t *testing.T, codec rpc.ClientCodec, token stri
} }
registerTestCatalogEntriesMap(t, codec, registrations) 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{ entries := []structs.ConfigEntryRequest{
{ {
Datacenter: "dc1", Datacenter: "dc1",
@ -868,6 +900,39 @@ func registerTestTopologyEntries(t *testing.T, codec rpc.ClientCodec, token stri
}, },
WriteRequest: structs.WriteRequest{Token: token}, 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", Datacenter: "dc1",
Entry: &structs.ServiceIntentionsConfigEntry{ Entry: &structs.ServiceIntentionsConfigEntry{

View File

@ -1690,20 +1690,67 @@ func TestInternal_ServiceTopology(t *testing.T) {
codec := rpcClient(t, s1) codec := rpcClient(t, s1)
defer codec.Close() 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 // 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, "") registerTestTopologyEntries(t, codec, "")
var ( var (
api = structs.NewServiceName("api", structs.DefaultEnterpriseMeta()) ingress = structs.NewServiceName("ingress", structs.DefaultEnterpriseMeta())
web = structs.NewServiceName("web", structs.DefaultEnterpriseMeta()) api = structs.NewServiceName("api", structs.DefaultEnterpriseMeta())
redis = structs.NewServiceName("redis", 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) { t.Run("api", func(t *testing.T) {
retry.Run(t, func(r *retry.R) { retry.Run(t, func(r *retry.R) {
args := structs.ServiceSpecificRequest{ args := structs.ServiceSpecificRequest{
@ -1715,12 +1762,11 @@ func TestInternal_ServiceTopology(t *testing.T) {
require.False(r, out.FilteredByACLs) require.False(r, out.FilteredByACLs)
require.Equal(r, "http", out.ServiceTopology.MetricsProtocol) require.Equal(r, "http", out.ServiceTopology.MetricsProtocol)
// bar/web, bar/web-proxy, baz/web, baz/web-proxy // edge/ingress
require.Len(r, out.ServiceTopology.Upstreams, 4) require.Len(r, out.ServiceTopology.Downstreams, 1)
require.Len(r, out.ServiceTopology.Downstreams, 0)
expectUp := map[string]structs.IntentionDecisionSummary{ expectDown := map[string]structs.IntentionDecisionSummary{
web.String(): { ingress.String(): {
DefaultAllow: true, DefaultAllow: true,
Allowed: false, Allowed: false,
HasPermissions: false, HasPermissions: false,
@ -1730,7 +1776,33 @@ func TestInternal_ServiceTopology(t *testing.T) {
HasExact: false, 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) 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{ expectDown := map[string]structs.IntentionDecisionSummary{
api.String(): { api.String(): {
DefaultAllow: true, DefaultAllow: true,
Allowed: false, Allowed: true,
HasPermissions: false, HasPermissions: false,
ExternalSource: "nomad", HasExact: true,
// From wildcard deny
HasExact: false,
}, },
} }
require.Equal(r, expectDown, out.ServiceTopology.DownstreamDecisions) 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 // zip/redis, zip/redis-proxy
require.Len(r, out.ServiceTopology.Upstreams, 2) require.Len(r, out.ServiceTopology.Upstreams, 2)
@ -1773,6 +1847,15 @@ func TestInternal_ServiceTopology(t *testing.T) {
}, },
} }
require.Equal(r, expectUp, out.ServiceTopology.UpstreamDecisions) 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) 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) codec := rpcClient(t, s1)
defer codec.Close() 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 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 // redis and redis-proxy on node zip
registerTestTopologyEntries(t, codec, TestDefaultMasterToken) registerTestTopologyEntries(t, codec, TestDefaultMasterToken)

View File

@ -2853,6 +2853,8 @@ func checkProtocolMatch(tx ReadTxn, ws memdb.WatchSet, svc *structs.GatewayServi
return idx, svc.Protocol == protocol, nil 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( func (s *Store) ServiceTopology(
ws memdb.WatchSet, ws memdb.WatchSet,
dc, service string, dc, service string,
@ -2863,14 +2865,15 @@ func (s *Store) ServiceTopology(
tx := s.db.ReadTxn() tx := s.db.ReadTxn()
defer tx.Abort() defer tx.Abort()
sn := structs.NewServiceName(service, entMeta)
var ( var (
maxIdx uint64 maxIdx uint64
protocol string protocol string
err error err error
fullyTransparent bool
sn = structs.NewServiceName(service, entMeta) hasTransparent bool
) )
switch kind { switch kind {
case structs.ServiceKindIngressGateway: case structs.ServiceKindIngressGateway:
maxIdx, protocol, err = metricsProtocolForIngressGateway(tx, ws, sn) 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) 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: default:
return 0, nil, fmt.Errorf("unsupported kind %q", kind) return 0, nil, fmt.Errorf("unsupported kind %q", kind)
} }
@ -2895,7 +2930,48 @@ func (s *Store) ServiceTopology(
if idx > maxIdx { if idx > maxIdx {
maxIdx = idx 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 { if err != nil {
return 0, nil, fmt.Errorf("failed to get upstreams for %q: %v", sn.String(), err) 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 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{ matchEntry := structs.IntentionMatchEntry{
Namespace: entMeta.NamespaceOrDefault(), Namespace: entMeta.NamespaceOrDefault(),
Name: service, Name: service,
} }
// The given service is a source relative to its upstreams _, srcIntentions, err := compatIntentionMatchOneTxn(
_, srcIntentions, err := compatIntentionMatchOneTxn(tx, ws, matchEntry, structs.IntentionMatchSource) tx,
ws,
matchEntry,
// The given service is a source relative to its upstreams
structs.IntentionMatchSource,
)
if err != nil { if err != nil {
return 0, nil, fmt.Errorf("failed to query intentions for %s", sn.String()) return 0, nil, fmt.Errorf("failed to query intentions for %s", sn.String())
} }
@ -2930,7 +3024,48 @@ func (s *Store) ServiceTopology(
if idx > maxIdx { if idx > maxIdx {
maxIdx = idx 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 { if err != nil {
return 0, nil, fmt.Errorf("failed to get downstreams for %q: %v", sn.String(), err) 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 maxIdx = idx
} }
// The given service is a destination relative to its downstreams // Store downstreams with at least one instance in transparent proxy mode.
_, dstIntentions, err := compatIntentionMatchOneTxn(tx, ws, matchEntry, structs.IntentionMatchDestination) // 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
structs.IntentionMatchDestination,
)
if err != nil { if err != nil {
return 0, nil, fmt.Errorf("failed to query intentions for %s", sn.String()) return 0, nil, fmt.Errorf("failed to query intentions for %s", sn.String())
} }
downstreamDecisions := make(map[string]structs.IntentionDecisionSummary)
for _, dn := range downstreamNames { for _, dn := range downstreamNames {
decision, err := s.IntentionDecision(dn.Name, dn.NamespaceOrDefault(), dstIntentions, structs.IntentionMatchSource, defaultAllow, false) decision, err := s.IntentionDecision(dn.Name, dn.NamespaceOrDefault(), dstIntentions, structs.IntentionMatchSource, defaultAllow, false)
if err != nil { if err != nil {
@ -2954,11 +3116,14 @@ func (s *Store) ServiceTopology(
} }
resp := &structs.ServiceTopology{ resp := &structs.ServiceTopology{
TransparentProxy: fullyTransparent,
MetricsProtocol: protocol, MetricsProtocol: protocol,
Upstreams: upstreams, Upstreams: upstreams,
Downstreams: downstreams, Downstreams: downstreams,
UpstreamDecisions: upstreamDecisions, UpstreamDecisions: upstreamDecisions,
DownstreamDecisions: downstreamDecisions, DownstreamDecisions: downstreamDecisions,
UpstreamSources: upstreamSources,
DownstreamSources: downstreamSources,
} }
return maxIdx, resp, nil 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. // 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. // 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) { 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 // First fetch services that have discovery chains that eventually route to the target service
idx, sources, err := s.discoveryChainSourcesTxn(tx, ws, dc, service) idx, sources, err := s.discoveryChainSourcesTxn(tx, ws, dc, service)

View File

@ -933,6 +933,11 @@ func intentionMatchGetParams(entry structs.IntentionMatchEntry) ([][]interface{}
return result, nil 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 // 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 // intentions. If intentions allow a connection from the target to some candidate service, the candidate service is considered
// an upstream of the target. // an upstream of the target.
@ -941,6 +946,25 @@ func (s *Store) IntentionTopology(ws memdb.WatchSet,
tx := s.db.ReadTxn() tx := s.db.ReadTxn()
defer tx.Abort() 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 var maxIdx uint64
// If querying the upstreams for a service, we first query intentions that apply to the target service as a source. // 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 { if downstreams {
decisionMatchType = structs.IntentionMatchSource decisionMatchType = structs.IntentionMatchSource
} }
result := make(structs.ServiceList, 0, len(allServices)) result := make([]ServiceWithDecision, 0, len(allServices))
for _, candidate := range allServices { for _, candidate := range allServices {
if candidate.Name == structs.ConsulServiceName { if candidate.Name == structs.ConsulServiceName {
continue continue
@ -1016,7 +1040,11 @@ func (s *Store) IntentionTopology(ws memdb.WatchSet,
if !decision.Allowed || target.Matches(candidate) { if !decision.Allowed || target.Matches(candidate) {
continue continue
} }
result = append(result, candidate)
result = append(result, ServiceWithDecision{
Name: candidate,
Decision: decision,
})
} }
return maxIdx, result, err return maxIdx, result, err
} }

View File

@ -35,6 +35,22 @@ const (
MeshGatewayModeRemote MeshGatewayMode = "remote" 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 // MeshGatewayConfig controls how Mesh Gateways are configured and used
// This is a struct to allow for future additions without having more free-hanging // This is a struct to allow for future additions without having more free-hanging
// configuration items all over the place // configuration items all over the place

View File

@ -1924,6 +1924,17 @@ type ServiceTopology struct {
// MetricsProtocol is the protocol of the service being queried // MetricsProtocol is the protocol of the service being queried
MetricsProtocol string 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 // IndexedConfigEntries has its own encoding logic which differs from

View File

@ -19,19 +19,21 @@ import (
// ServiceSummary is used to summarize a service // ServiceSummary is used to summarize a service
type ServiceSummary struct { type ServiceSummary struct {
Kind structs.ServiceKind `json:",omitempty"` Kind structs.ServiceKind `json:",omitempty"`
Name string Name string
Datacenter string Datacenter string
Tags []string Tags []string
Nodes []string Nodes []string
ExternalSources []string ExternalSources []string
externalSourceSet map[string]struct{} // internal to track uniqueness externalSourceSet map[string]struct{} // internal to track uniqueness
checks map[string]*structs.HealthCheck checks map[string]*structs.HealthCheck
InstanceCount int InstanceCount int
ChecksPassing int ChecksPassing int
ChecksWarning int ChecksWarning int
ChecksCritical int ChecksCritical int
GatewayConfig GatewayConfig GatewayConfig GatewayConfig
TransparentProxy bool
transparentProxySet bool
structs.EnterpriseMeta structs.EnterpriseMeta
} }
@ -61,14 +63,16 @@ type ServiceListingSummary struct {
type ServiceTopologySummary struct { type ServiceTopologySummary struct {
ServiceSummary ServiceSummary
Source string
Intention structs.IntentionDecisionSummary Intention structs.IntentionDecisionSummary
} }
type ServiceTopology struct { type ServiceTopology struct {
Protocol string Protocol string
Upstreams []*ServiceTopologySummary TransparentProxy bool
Downstreams []*ServiceTopologySummary Upstreams []*ServiceTopologySummary
FilteredByACLs bool Downstreams []*ServiceTopologySummary
FilteredByACLs bool
} }
// UINodes is used to list the nodes in a given datacenter. We return a // UINodes is used to list the nodes in a given datacenter. We return a
@ -334,6 +338,7 @@ RPC:
sum := ServiceTopologySummary{ sum := ServiceTopologySummary{
ServiceSummary: *svc, ServiceSummary: *svc,
Intention: out.ServiceTopology.UpstreamDecisions[sn.String()], Intention: out.ServiceTopology.UpstreamDecisions[sn.String()],
Source: out.ServiceTopology.UpstreamSources[sn.String()],
} }
upstreamResp = append(upstreamResp, &sum) upstreamResp = append(upstreamResp, &sum)
} }
@ -344,15 +349,17 @@ RPC:
sum := ServiceTopologySummary{ sum := ServiceTopologySummary{
ServiceSummary: *svc, ServiceSummary: *svc,
Intention: out.ServiceTopology.DownstreamDecisions[sn.String()], Intention: out.ServiceTopology.DownstreamDecisions[sn.String()],
Source: out.ServiceTopology.DownstreamSources[sn.String()],
} }
downstreamResp = append(downstreamResp, &sum) downstreamResp = append(downstreamResp, &sum)
} }
topo := ServiceTopology{ topo := ServiceTopology{
Protocol: out.ServiceTopology.MetricsProtocol, TransparentProxy: out.ServiceTopology.TransparentProxy,
Upstreams: upstreamResp, Protocol: out.ServiceTopology.MetricsProtocol,
Downstreams: downstreamResp, Upstreams: upstreamResp,
FilteredByACLs: out.FilteredByACLs, Downstreams: downstreamResp,
FilteredByACLs: out.FilteredByACLs,
} }
return topo, nil return topo, nil
} }
@ -410,6 +417,17 @@ func summarizeServices(dump structs.ServiceDump, cfg *config.RuntimeConfig, dc s
} }
destination.checks[uid] = check 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 { for _, tag := range svc.Tags {
found := false found := false

View File

@ -396,6 +396,7 @@ func TestUiServices(t *testing.T) {
// internal accounting that users don't see can be blown away // internal accounting that users don't see can be blown away
for _, sum := range summary { for _, sum := range summary {
sum.transparentProxySet = false
sum.externalSourceSet = nil sum.externalSourceSet = nil
sum.checks = nil sum.checks = nil
} }
@ -1078,12 +1079,7 @@ func TestUIServiceTopology(t *testing.T) {
Address: "198.18.1.2", Address: "198.18.1.2",
Proxy: structs.ConnectProxyConfig{ Proxy: structs.ConnectProxyConfig{
DestinationServiceName: "api", DestinationServiceName: "api",
Upstreams: structs.Upstreams{ Mode: structs.ProxyModeTransparent,
{
DestinationName: "web",
LocalBindPort: 8080,
},
},
}, },
}, },
Checks: structs.HealthChecks{ Checks: structs.HealthChecks{
@ -1210,12 +1206,7 @@ func TestUIServiceTopology(t *testing.T) {
Address: "198.18.1.40", Address: "198.18.1.40",
Proxy: structs.ConnectProxyConfig{ Proxy: structs.ConnectProxyConfig{
DestinationServiceName: "web", DestinationServiceName: "web",
Upstreams: structs.Upstreams{ Mode: structs.ProxyModeTransparent,
{
DestinationName: "redis",
LocalBindPort: 123,
},
},
}, },
}, },
Checks: structs.HealthChecks{ 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 // ingress -> api gateway config entry (but no intention)
// Add ingress config: ingress -> api // wildcard deny intention
// api -> web exact intention
// web -> redis exact intention
{ {
entries := []structs.ConfigEntryRequest{ entries := []structs.ConfigEntryRequest{
{ {
@ -1318,6 +1311,20 @@ func TestUIServiceTopology(t *testing.T) {
Protocol: "tcp", 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", Datacenter: "dc1",
Entry: &structs.ServiceIntentionsConfigEntry{ Entry: &structs.ServiceIntentionsConfigEntry{
@ -1342,12 +1349,11 @@ func TestUIServiceTopology(t *testing.T) {
Datacenter: "dc1", Datacenter: "dc1",
Entry: &structs.ServiceIntentionsConfigEntry{ Entry: &structs.ServiceIntentionsConfigEntry{
Kind: structs.ServiceIntentions, Kind: structs.ServiceIntentions,
Name: "*", Name: "web",
Meta: map[string]string{structs.MetaExternalSource: "nomad"},
Sources: []*structs.SourceIntention{ Sources: []*structs.SourceIntention{
{ {
Name: "*", Action: structs.IntentionActionAllow,
Action: structs.IntentionActionDeny, Name: "api",
}, },
}, },
}, },
@ -1419,16 +1425,18 @@ func TestUIServiceTopology(t *testing.T) {
require.NoError(r, checkIndex(resp)) require.NoError(r, checkIndex(resp))
expect := ServiceTopology{ expect := ServiceTopology{
Protocol: "tcp", Protocol: "tcp",
TransparentProxy: false,
Upstreams: []*ServiceTopologySummary{ Upstreams: []*ServiceTopologySummary{
{ {
ServiceSummary: ServiceSummary{ ServiceSummary: ServiceSummary{
Name: "api", Name: "api",
Datacenter: "dc1", Datacenter: "dc1",
Nodes: []string{"foo"}, Nodes: []string{"foo"},
InstanceCount: 1, InstanceCount: 1,
ChecksPassing: 3, ChecksPassing: 3,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(), EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
TransparentProxy: true,
}, },
Intention: structs.IntentionDecisionSummary{ Intention: structs.IntentionDecisionSummary{
DefaultAllow: true, DefaultAllow: true,
@ -1436,6 +1444,7 @@ func TestUIServiceTopology(t *testing.T) {
HasPermissions: false, HasPermissions: false,
HasExact: true, HasExact: true,
}, },
Source: structs.TopologySourceRegistration,
}, },
}, },
Downstreams: []*ServiceTopologySummary{}, Downstreams: []*ServiceTopologySummary{},
@ -1447,6 +1456,7 @@ func TestUIServiceTopology(t *testing.T) {
for _, u := range result.Upstreams { for _, u := range result.Upstreams {
u.externalSourceSet = nil u.externalSourceSet = nil
u.checks = nil u.checks = nil
u.transparentProxySet = false
} }
require.Equal(r, expect, result) require.Equal(r, expect, result)
}) })
@ -1462,17 +1472,19 @@ func TestUIServiceTopology(t *testing.T) {
require.NoError(r, checkIndex(resp)) require.NoError(r, checkIndex(resp))
expect := ServiceTopology{ expect := ServiceTopology{
Protocol: "tcp", Protocol: "tcp",
TransparentProxy: true,
Downstreams: []*ServiceTopologySummary{ Downstreams: []*ServiceTopologySummary{
{ {
ServiceSummary: ServiceSummary{ ServiceSummary: ServiceSummary{
Name: "ingress", Name: "ingress",
Kind: structs.ServiceKindIngressGateway, Kind: structs.ServiceKindIngressGateway,
Datacenter: "dc1", Datacenter: "dc1",
Nodes: []string{"edge"}, Nodes: []string{"edge"},
InstanceCount: 1, InstanceCount: 1,
ChecksPassing: 1, ChecksPassing: 1,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(), EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
TransparentProxy: false,
}, },
Intention: structs.IntentionDecisionSummary{ Intention: structs.IntentionDecisionSummary{
DefaultAllow: true, DefaultAllow: true,
@ -1480,29 +1492,29 @@ func TestUIServiceTopology(t *testing.T) {
HasPermissions: false, HasPermissions: false,
HasExact: true, HasExact: true,
}, },
Source: structs.TopologySourceRegistration,
}, },
}, },
Upstreams: []*ServiceTopologySummary{ Upstreams: []*ServiceTopologySummary{
{ {
ServiceSummary: ServiceSummary{ ServiceSummary: ServiceSummary{
Name: "web", Name: "web",
Datacenter: "dc1", Datacenter: "dc1",
Nodes: []string{"bar", "baz"}, Nodes: []string{"bar", "baz"},
InstanceCount: 2, InstanceCount: 2,
ChecksPassing: 3, ChecksPassing: 3,
ChecksWarning: 1, ChecksWarning: 1,
ChecksCritical: 2, ChecksCritical: 2,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(), EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
TransparentProxy: false,
}, },
Intention: structs.IntentionDecisionSummary{ Intention: structs.IntentionDecisionSummary{
DefaultAllow: true, DefaultAllow: true,
Allowed: false, Allowed: true,
HasPermissions: false, HasPermissions: false,
ExternalSource: "nomad", HasExact: true,
// From wildcard deny
HasExact: false,
}, },
Source: structs.TopologySourceSpecificIntention,
}, },
}, },
FilteredByACLs: false, FilteredByACLs: false,
@ -1511,10 +1523,12 @@ func TestUIServiceTopology(t *testing.T) {
// Internal accounting that is not returned in JSON response // Internal accounting that is not returned in JSON response
for _, u := range result.Upstreams { for _, u := range result.Upstreams {
u.transparentProxySet = false
u.externalSourceSet = nil u.externalSourceSet = nil
u.checks = nil u.checks = nil
} }
for _, d := range result.Downstreams { for _, d := range result.Downstreams {
d.transparentProxySet = false
d.externalSourceSet = nil d.externalSourceSet = nil
d.checks = nil d.checks = nil
} }
@ -1532,17 +1546,19 @@ func TestUIServiceTopology(t *testing.T) {
require.NoError(r, checkIndex(resp)) require.NoError(r, checkIndex(resp))
expect := ServiceTopology{ expect := ServiceTopology{
Protocol: "http", Protocol: "http",
TransparentProxy: false,
Upstreams: []*ServiceTopologySummary{ Upstreams: []*ServiceTopologySummary{
{ {
ServiceSummary: ServiceSummary{ ServiceSummary: ServiceSummary{
Name: "redis", Name: "redis",
Datacenter: "dc1", Datacenter: "dc1",
Nodes: []string{"zip"}, Nodes: []string{"zip"},
InstanceCount: 1, InstanceCount: 1,
ChecksPassing: 2, ChecksPassing: 2,
ChecksCritical: 1, ChecksCritical: 1,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(), EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
TransparentProxy: false,
}, },
Intention: structs.IntentionDecisionSummary{ Intention: structs.IntentionDecisionSummary{
DefaultAllow: true, DefaultAllow: true,
@ -1550,27 +1566,27 @@ func TestUIServiceTopology(t *testing.T) {
HasPermissions: true, HasPermissions: true,
HasExact: true, HasExact: true,
}, },
Source: structs.TopologySourceRegistration,
}, },
}, },
Downstreams: []*ServiceTopologySummary{ Downstreams: []*ServiceTopologySummary{
{ {
ServiceSummary: ServiceSummary{ ServiceSummary: ServiceSummary{
Name: "api", Name: "api",
Datacenter: "dc1", Datacenter: "dc1",
Nodes: []string{"foo"}, Nodes: []string{"foo"},
InstanceCount: 1, InstanceCount: 1,
ChecksPassing: 3, ChecksPassing: 3,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(), EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
TransparentProxy: true,
}, },
Intention: structs.IntentionDecisionSummary{ Intention: structs.IntentionDecisionSummary{
DefaultAllow: true, DefaultAllow: true,
Allowed: false, Allowed: true,
HasPermissions: false, HasPermissions: false,
ExternalSource: "nomad", HasExact: true,
// From wildcard deny
HasExact: false,
}, },
Source: structs.TopologySourceSpecificIntention,
}, },
}, },
FilteredByACLs: false, FilteredByACLs: false,
@ -1579,10 +1595,12 @@ func TestUIServiceTopology(t *testing.T) {
// Internal accounting that is not returned in JSON response // Internal accounting that is not returned in JSON response
for _, u := range result.Upstreams { for _, u := range result.Upstreams {
u.transparentProxySet = false
u.externalSourceSet = nil u.externalSourceSet = nil
u.checks = nil u.checks = nil
} }
for _, d := range result.Downstreams { for _, d := range result.Downstreams {
d.transparentProxySet = false
d.externalSourceSet = nil d.externalSourceSet = nil
d.checks = nil d.checks = nil
} }
@ -1620,6 +1638,7 @@ func TestUIServiceTopology(t *testing.T) {
HasPermissions: true, HasPermissions: true,
HasExact: true, HasExact: true,
}, },
Source: structs.TopologySourceRegistration,
}, },
}, },
FilteredByACLs: false, FilteredByACLs: false,
@ -1628,6 +1647,7 @@ func TestUIServiceTopology(t *testing.T) {
// Internal accounting that is not returned in JSON response // Internal accounting that is not returned in JSON response
for _, d := range result.Downstreams { for _, d := range result.Downstreams {
d.transparentProxySet = false
d.externalSourceSet = nil d.externalSourceSet = nil
d.checks = nil d.checks = nil
} }