Add state store function for intention upstreams

This commit is contained in:
freddygv 2021-03-13 21:44:01 -07:00
parent 4976c000b7
commit e4e14639b2
3 changed files with 468 additions and 4 deletions

View file

@ -724,14 +724,16 @@ func (s *Store) Services(ws memdb.WatchSet, entMeta *structs.EnterpriseMeta) (ui
return idx, results, nil
}
func (s *Store) ServiceList(ws memdb.WatchSet, entMeta *structs.EnterpriseMeta) (uint64, structs.ServiceList, error) {
func (s *Store) ServiceList(ws memdb.WatchSet,
include func(svc *structs.ServiceNode) bool, entMeta *structs.EnterpriseMeta) (uint64, structs.ServiceList, error) {
tx := s.db.Txn(false)
defer tx.Abort()
return serviceListTxn(tx, ws, entMeta)
return serviceListTxn(tx, ws, include, entMeta)
}
func serviceListTxn(tx ReadTxn, ws memdb.WatchSet, entMeta *structs.EnterpriseMeta) (uint64, structs.ServiceList, error) {
func serviceListTxn(tx ReadTxn, ws memdb.WatchSet,
include func(svc *structs.ServiceNode) bool, entMeta *structs.EnterpriseMeta) (uint64, structs.ServiceList, error) {
idx := catalogServicesMaxIndex(tx, entMeta)
services, err := catalogServiceList(tx, entMeta, true)
@ -743,7 +745,11 @@ func serviceListTxn(tx ReadTxn, ws memdb.WatchSet, entMeta *structs.EnterpriseMe
unique := make(map[structs.ServiceName]struct{})
for service := services.Next(); service != nil; service = services.Next() {
svc := service.(*structs.ServiceNode)
unique[svc.CompoundServiceName()] = struct{}{}
// TODO (freddy) This is a hack to exclude certain kinds.
// Need a new index to query by kind and namespace, have to coordinate with consul foundations first
if include != nil && include(svc) {
unique[svc.CompoundServiceName()] = struct{}{}
}
}
results := make(structs.ServiceList, 0, len(unique))

View file

@ -950,3 +950,88 @@ func intentionMatchGetParams(entry structs.IntentionMatchEntry) ([][]interface{}
result = append(result, []interface{}{entry.Namespace, entry.Name})
return result, nil
}
// 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.
func (s *Store) IntentionTopology(ws memdb.WatchSet,
target structs.ServiceName, downstreams bool, defaultDecision acl.EnforcementDecision) (uint64, structs.ServiceList, error) {
var maxIdx uint64
// If querying the upstreams for a service, we first query intentions that apply to the target service as a source.
// That way we can check whether intentions from the source allow connections to upstream candidates.
matchType := structs.IntentionMatchSource
if downstreams {
matchType = structs.IntentionMatchDestination
}
entry := structs.IntentionMatchEntry{
Namespace: target.NamespaceOrDefault(),
Name: target.Name,
}
index, intentions, err := s.IntentionMatchOne(ws, entry, matchType)
if err != nil {
return 0, nil, fmt.Errorf("failed to query intentions for %s", target.String())
}
if index > maxIdx {
maxIdx = index
}
// Reset the matchType since next it is used for evaluating the upstreams or downstreams against a set of intentions.
// When evaluating upstreams, the match type is now destination because we are evaluating upstream candidates
// as eligible destinations for intentions that have the target service as a source.
// The reverse is true for downstreams.
matchType = structs.IntentionMatchDestination
if downstreams {
matchType = structs.IntentionMatchSource
}
// Check for a wildcard intention (* -> *) since it overrides the default decision from ACLs
if len(intentions) > 0 {
// Intentions with wildcard source and destination have the lowest precedence, so they are last in the list
ixn := intentions[len(intentions)-1]
// TODO (freddy) This needs an enterprise split to account for (*/* -> */*)
// Maybe ixn.HasWildcardSource() && ixn.HasWildcardDestination()
if ixn.SourceName == structs.WildcardSpecifier && ixn.DestinationName == structs.WildcardSpecifier {
defaultDecision = acl.Allow
if ixn.Action == structs.IntentionActionDeny {
defaultDecision = acl.Deny
}
}
}
index, allServices, err := s.ServiceList(ws, func(svc *structs.ServiceNode) bool {
// Only include ingress gateways as downstreams, since they cannot receive service mesh traffic
// TODO(freddy): One remaining issue is that this includes non-Connect services (typical services without a proxy)
// Ideally those should be excluded as well, since they can't be upstreams/downstreams without a proxy.
// Maybe start tracking services represented by proxies? (both sidecar and ingress)
if svc.ServiceKind == structs.ServiceKindTypical || (svc.ServiceKind == structs.ServiceKindIngressGateway && downstreams) {
return true
}
return false
}, structs.WildcardEnterpriseMeta())
if err != nil {
return index, nil, fmt.Errorf("failed to fetch catalog service list: %v", err)
}
if index > maxIdx {
maxIdx = index
}
result := make(structs.ServiceList, 0, len(allServices))
for _, candidate := range allServices {
decision, err := s.IntentionDecision(candidate.Name, candidate.NamespaceOrDefault(), intentions, matchType, defaultDecision, true)
if err != nil {
src, dst := target, candidate
if downstreams {
src, dst = candidate, target
}
return 0, nil, fmt.Errorf("failed to get intention decision from (%s) to (%s): %v",
src.String(), dst.String(), err)
}
if !decision.Allowed || target.Matches(candidate) {
continue
}
result = append(result, candidate)
}
return maxIdx, result, err
}

View file

@ -1,6 +1,7 @@
package state
import (
"sort"
"testing"
"time"
@ -2057,3 +2058,375 @@ func testConfigStateStore(t *testing.T) *Store {
disableLegacyIntentions(s)
return s
}
func TestStore_IntentionTopology(t *testing.T) {
node := structs.Node{
Node: "foo",
Address: "127.0.0.1",
}
services := []structs.NodeService{
{
ID: "api-1",
Service: "api",
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
{
ID: "mysql-1",
Service: "mysql",
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
{
ID: "web-1",
Service: "web",
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
{
Kind: structs.ServiceKindConnectProxy,
ID: "web-proxy-1",
Service: "web-proxy",
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
{
Kind: structs.ServiceKindTerminatingGateway,
ID: "terminating-gateway-1",
Service: "terminating-gateway",
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
{
Kind: structs.ServiceKindIngressGateway,
ID: "ingress-gateway-1",
Service: "ingress-gateway",
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
{
Kind: structs.ServiceKindMeshGateway,
ID: "mesh-gateway-1",
Service: "mesh-gateway",
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
}
type expect struct {
idx uint64
services structs.ServiceList
}
tests := []struct {
name string
defaultDecision acl.EnforcementDecision
intentions []structs.ServiceIntentionsConfigEntry
target structs.ServiceName
downstreams bool
expect expect
}{
{
name: "(upstream) acl allow all but intentions deny one",
defaultDecision: acl.Allow,
intentions: []structs.ServiceIntentionsConfigEntry{
{
Kind: structs.ServiceIntentions,
Name: "api",
Sources: []*structs.SourceIntention{
{
Name: "web",
Action: structs.IntentionActionDeny,
},
},
},
},
target: structs.NewServiceName("web", nil),
downstreams: false,
expect: expect{
idx: 9,
services: structs.ServiceList{
{
Name: "mysql",
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
},
},
},
{
name: "(upstream) acl deny all intentions allow one",
defaultDecision: acl.Deny,
intentions: []structs.ServiceIntentionsConfigEntry{
{
Kind: structs.ServiceIntentions,
Name: "api",
Sources: []*structs.SourceIntention{
{
Name: "web",
Action: structs.IntentionActionAllow,
},
},
},
},
target: structs.NewServiceName("web", nil),
downstreams: false,
expect: expect{
idx: 9,
services: structs.ServiceList{
{
Name: "api",
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
},
},
},
{
name: "(downstream) acl allow all but intentions deny one",
defaultDecision: acl.Allow,
intentions: []structs.ServiceIntentionsConfigEntry{
{
Kind: structs.ServiceIntentions,
Name: "api",
Sources: []*structs.SourceIntention{
{
Name: "web",
Action: structs.IntentionActionDeny,
},
},
},
},
target: structs.NewServiceName("api", nil),
downstreams: true,
expect: expect{
idx: 9,
services: structs.ServiceList{
{
Name: "ingress-gateway",
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
{
Name: "mysql",
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
},
},
},
{
name: "(downstream) acl deny all intentions allow one",
defaultDecision: acl.Deny,
intentions: []structs.ServiceIntentionsConfigEntry{
{
Kind: structs.ServiceIntentions,
Name: "api",
Sources: []*structs.SourceIntention{
{
Name: "web",
Action: structs.IntentionActionAllow,
},
},
},
},
target: structs.NewServiceName("api", nil),
downstreams: true,
expect: expect{
idx: 9,
services: structs.ServiceList{
{
Name: "web",
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
},
},
},
{
name: "acl deny but intention allow all overrides it",
defaultDecision: acl.Deny,
intentions: []structs.ServiceIntentionsConfigEntry{
{
Kind: structs.ServiceIntentions,
Name: "*",
Sources: []*structs.SourceIntention{
{
Name: "*",
Action: structs.IntentionActionAllow,
},
},
},
},
target: structs.NewServiceName("web", nil),
downstreams: false,
expect: expect{
idx: 9,
services: structs.ServiceList{
{
Name: "api",
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
{
Name: "mysql",
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
},
},
},
{
name: "acl allow but intention deny all overrides it",
defaultDecision: acl.Allow,
intentions: []structs.ServiceIntentionsConfigEntry{
{
Kind: structs.ServiceIntentions,
Name: "*",
Sources: []*structs.SourceIntention{
{
Name: "*",
Action: structs.IntentionActionDeny,
},
},
},
},
target: structs.NewServiceName("web", nil),
downstreams: false,
expect: expect{
idx: 9,
services: structs.ServiceList{},
},
},
{
name: "acl deny but intention allow all overrides it",
defaultDecision: acl.Deny,
intentions: []structs.ServiceIntentionsConfigEntry{
{
Kind: structs.ServiceIntentions,
Name: "*",
Sources: []*structs.SourceIntention{
{
Name: "*",
Action: structs.IntentionActionAllow,
},
},
},
},
target: structs.NewServiceName("web", nil),
downstreams: false,
expect: expect{
idx: 9,
services: structs.ServiceList{
{
Name: "api",
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
{
Name: "mysql",
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := testConfigStateStore(t)
var idx uint64 = 1
require.NoError(t, s.EnsureNode(idx, &node))
idx++
for _, svc := range services {
require.NoError(t, s.EnsureService(idx, "foo", &svc))
idx++
}
for _, ixn := range tt.intentions {
require.NoError(t, s.EnsureConfigEntry(idx, &ixn))
idx++
}
idx, got, err := s.IntentionTopology(nil, tt.target, tt.downstreams, tt.defaultDecision)
require.NoError(t, err)
require.Equal(t, tt.expect.idx, idx)
// ServiceList is from a map, so it is not deterministically sorted
sort.Slice(got, func(i, j int) bool {
return got[i].String() < got[j].String()
})
require.Equal(t, tt.expect.services, got)
})
}
}
func TestStore_IntentionTopology_Watches(t *testing.T) {
s := testConfigStateStore(t)
var i uint64 = 1
require.NoError(t, s.EnsureNode(i, &structs.Node{
Node: "foo",
Address: "127.0.0.1",
}))
i++
target := structs.NewServiceName("web", structs.DefaultEnterpriseMeta())
ws := memdb.NewWatchSet()
index, got, err := s.IntentionTopology(ws, target, false, acl.Deny)
require.NoError(t, err)
require.Equal(t, uint64(0), index)
require.Empty(t, got)
// Watch should fire after adding a relevant config entry
require.NoError(t, s.EnsureConfigEntry(i, &structs.ServiceIntentionsConfigEntry{
Kind: structs.ServiceIntentions,
Name: "api",
Sources: []*structs.SourceIntention{
{
Name: "web",
Action: structs.IntentionActionAllow,
},
},
}))
i++
require.True(t, watchFired(ws))
// Reset the WatchSet
ws = memdb.NewWatchSet()
index, got, err = s.IntentionTopology(ws, target, false, acl.Deny)
require.NoError(t, err)
require.Equal(t, uint64(2), index)
require.Empty(t, got)
// Watch should not fire after unrelated intention changes
require.NoError(t, s.EnsureConfigEntry(i, &structs.ServiceIntentionsConfigEntry{
Kind: structs.ServiceIntentions,
Name: "another service",
Sources: []*structs.SourceIntention{
{
Name: "any other service",
Action: structs.IntentionActionAllow,
},
},
}))
i++
// TODO(freddy) Why is this firing?
require.False(t, watchFired(ws))
// Result should not have changed
index, got, err = s.IntentionTopology(ws, target, false, acl.Deny)
require.NoError(t, err)
require.Equal(t, uint64(3), index)
require.Empty(t, got)
// Watch should fire after service list changes
require.NoError(t, s.EnsureService(i, "foo", &structs.NodeService{
ID: "api-1",
Service: "api",
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
}))
i++
require.True(t, watchFired(ws))
// Reset the WatchSet
index, got, err = s.IntentionTopology(nil, target, false, acl.Deny)
require.NoError(t, err)
require.Equal(t, uint64(4), index)
expect := structs.ServiceList{
{
Name: "api",
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
}
require.Equal(t, expect, got)
}