Add state store function for intention upstreams
This commit is contained in:
parent
4976c000b7
commit
e4e14639b2
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue