mirror of https://github.com/status-im/consul.git
Return intention info in svc topology endpoint (#8853)
This commit is contained in:
parent
6c0907f494
commit
da91e999f6
|
@ -125,13 +125,9 @@ func (a *Agent) ConnectAuthorize(token string,
|
||||||
}
|
}
|
||||||
|
|
||||||
// No match, we need to determine the default behavior. We do this by
|
// No match, we need to determine the default behavior. We do this by
|
||||||
// specifying the anonymous token, which will get the default behavior. The
|
// fetching the default intention behavior from the resolved authorizer. The
|
||||||
// default behavior if ACLs are disabled is to allow connections to mimic the
|
// default behavior if ACLs are disabled is to allow connections to mimic the
|
||||||
// behavior of Consul itself: everything is allowed if ACLs are disabled.
|
// behavior of Consul itself: everything is allowed if ACLs are disabled.
|
||||||
authz, err = a.resolveToken("")
|
|
||||||
if err != nil {
|
|
||||||
return returnErr(err)
|
|
||||||
}
|
|
||||||
if authz == nil {
|
if authz == nil {
|
||||||
// ACLs not enabled at all, the default is allow all.
|
// ACLs not enabled at all, the default is allow all.
|
||||||
return true, "ACLs disabled, access is allowed by default", &meta, nil
|
return true, "ACLs disabled, access is allowed by default", &meta, nil
|
||||||
|
|
|
@ -844,4 +844,59 @@ 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
|
||||||
|
entries := []structs.ConfigEntryRequest{
|
||||||
|
{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Entry: &structs.ProxyConfigEntry{
|
||||||
|
Kind: structs.ProxyDefaults,
|
||||||
|
Name: structs.ProxyConfigGlobal,
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"protocol": "http",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
WriteRequest: structs.WriteRequest{Token: token},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Entry: &structs.ServiceIntentionsConfigEntry{
|
||||||
|
Kind: structs.ServiceIntentions,
|
||||||
|
Name: "redis",
|
||||||
|
Sources: []*structs.SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "web",
|
||||||
|
Permissions: []*structs.IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: structs.IntentionActionAllow,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
Methods: []string{"GET"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
WriteRequest: structs.WriteRequest{Token: token},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Entry: &structs.ServiceIntentionsConfigEntry{
|
||||||
|
Kind: structs.ServiceIntentions,
|
||||||
|
Name: "*",
|
||||||
|
Meta: map[string]string{structs.MetaExternalSource: "nomad"},
|
||||||
|
Sources: []*structs.SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "*",
|
||||||
|
Action: structs.IntentionActionDeny,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
WriteRequest: structs.WriteRequest{Token: token},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, req := range entries {
|
||||||
|
var out bool
|
||||||
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &req, &out))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -783,53 +783,11 @@ func (s *Intention) Check(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the matches for this destination
|
|
||||||
state := s.srv.fsm.State()
|
|
||||||
_, matches, err := state.IntentionMatch(nil, &structs.IntentionQueryMatch{
|
|
||||||
Type: structs.IntentionMatchDestination,
|
|
||||||
Entries: []structs.IntentionMatchEntry{
|
|
||||||
{
|
|
||||||
Namespace: query.DestinationNS,
|
|
||||||
Name: query.DestinationName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(matches) != 1 {
|
|
||||||
// This should never happen since the documented behavior of the
|
|
||||||
// Match call is that it'll always return exactly the number of results
|
|
||||||
// as entries passed in. But we guard against misbehavior.
|
|
||||||
return errors.New("internal error loading matches")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Figure out which source matches this request.
|
|
||||||
var ixnMatch *structs.Intention
|
|
||||||
for _, ixn := range matches[0] {
|
|
||||||
if _, ok := uri.Authorize(ixn); ok {
|
|
||||||
ixnMatch = ixn
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ixnMatch != nil {
|
|
||||||
if len(ixnMatch.Permissions) == 0 {
|
|
||||||
// This is an L4 intention.
|
|
||||||
reply.Allowed = ixnMatch.Action == structs.IntentionActionAllow
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is an L7 intention, so DENY.
|
|
||||||
reply.Allowed = false
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: the default intention policy is like an intention with a
|
// Note: the default intention policy is like an intention with a
|
||||||
// wildcarded destination in that it is limited to L4-only.
|
// wildcarded destination in that it is limited to L4-only.
|
||||||
|
|
||||||
// No match, we need to determine the default behavior. We do this by
|
// No match, we need to determine the default behavior. We do this by
|
||||||
// specifying the anonymous token token, which will get that behavior.
|
// fetching the default intention behavior from the resolved authorizer.
|
||||||
// The default behavior if ACLs are disabled is to allow connections
|
// The default behavior if ACLs are disabled is to allow connections
|
||||||
// to mimic the behavior of Consul itself: everything is allowed if
|
// to mimic the behavior of Consul itself: everything is allowed if
|
||||||
// ACLs are disabled.
|
// ACLs are disabled.
|
||||||
|
@ -837,15 +795,18 @@ func (s *Intention) Check(
|
||||||
// NOTE(mitchellh): This is the same behavior as the agent authorize
|
// NOTE(mitchellh): This is the same behavior as the agent authorize
|
||||||
// endpoint. If this behavior is incorrect, we should also change it there
|
// endpoint. If this behavior is incorrect, we should also change it there
|
||||||
// which is much more important.
|
// which is much more important.
|
||||||
authz, err = s.srv.ResolveToken("")
|
defaultDecision := acl.Allow
|
||||||
if err != nil {
|
if authz != nil {
|
||||||
return err
|
defaultDecision = authz.IntentionDefaultAllow(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
reply.Allowed = true
|
state := s.srv.fsm.State()
|
||||||
if authz != nil {
|
decision, err := state.IntentionDecision(uri, query.DestinationName, query.DestinationNS, defaultDecision)
|
||||||
reply.Allowed = authz.IntentionDefaultAllow(nil) == acl.Allow
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get intention decision from (%s/%s) to (%s/%s): %v",
|
||||||
|
query.SourceNS, query.SourceName, query.DestinationNS, query.DestinationName, err)
|
||||||
}
|
}
|
||||||
|
reply.Allowed = decision.Allowed
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,7 +168,12 @@ func (m *Internal) ServiceTopology(args *structs.ServiceSpecificRequest, reply *
|
||||||
&args.QueryOptions,
|
&args.QueryOptions,
|
||||||
&reply.QueryMeta,
|
&reply.QueryMeta,
|
||||||
func(ws memdb.WatchSet, state *state.Store) error {
|
func(ws memdb.WatchSet, state *state.Store) error {
|
||||||
index, topology, err := state.ServiceTopology(ws, args.Datacenter, args.ServiceName, &args.EnterpriseMeta)
|
defaultAllow := acl.Allow
|
||||||
|
if authz != nil {
|
||||||
|
defaultAllow = authz.IntentionDefaultAllow(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
index, topology, err := state.ServiceTopology(ws, args.Datacenter, args.ServiceName, defaultAllow, &args.EnterpriseMeta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
"github.com/hashicorp/consul/api"
|
"github.com/hashicorp/consul/api"
|
||||||
"github.com/hashicorp/consul/lib/stringslice"
|
"github.com/hashicorp/consul/lib/stringslice"
|
||||||
|
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||||
"github.com/hashicorp/consul/testrpc"
|
"github.com/hashicorp/consul/testrpc"
|
||||||
"github.com/hashicorp/consul/types"
|
"github.com/hashicorp/consul/types"
|
||||||
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
|
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
|
||||||
|
@ -1623,49 +1624,95 @@ func TestInternal_ServiceTopology(t *testing.T) {
|
||||||
// redis and redis-proxy on node zip
|
// redis and redis-proxy on node zip
|
||||||
registerTestTopologyEntries(t, codec, "")
|
registerTestTopologyEntries(t, codec, "")
|
||||||
|
|
||||||
|
var (
|
||||||
|
api = structs.NewServiceName("api", structs.DefaultEnterpriseMeta())
|
||||||
|
web = structs.NewServiceName("web", structs.DefaultEnterpriseMeta())
|
||||||
|
redis = structs.NewServiceName("redis", structs.DefaultEnterpriseMeta())
|
||||||
|
)
|
||||||
|
|
||||||
t.Run("api", func(t *testing.T) {
|
t.Run("api", func(t *testing.T) {
|
||||||
|
retry.Run(t, func(r *retry.R) {
|
||||||
args := structs.ServiceSpecificRequest{
|
args := structs.ServiceSpecificRequest{
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
ServiceName: "api",
|
ServiceName: "api",
|
||||||
}
|
}
|
||||||
var out structs.IndexedServiceTopology
|
var out structs.IndexedServiceTopology
|
||||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.ServiceTopology", &args, &out))
|
require.NoError(r, msgpackrpc.CallWithCodec(codec, "Internal.ServiceTopology", &args, &out))
|
||||||
require.False(t, out.FilteredByACLs)
|
require.False(r, out.FilteredByACLs)
|
||||||
|
|
||||||
// bar/web, bar/web-proxy, baz/web, baz/web-proxy
|
// bar/web, bar/web-proxy, baz/web, baz/web-proxy
|
||||||
require.Len(t, out.ServiceTopology.Upstreams, 4)
|
require.Len(r, out.ServiceTopology.Upstreams, 4)
|
||||||
require.Len(t, out.ServiceTopology.Downstreams, 0)
|
require.Len(r, out.ServiceTopology.Downstreams, 0)
|
||||||
|
|
||||||
|
expectUp := map[string]structs.IntentionDecisionSummary{
|
||||||
|
web.String(): {
|
||||||
|
Allowed: false,
|
||||||
|
HasPermissions: false,
|
||||||
|
ExternalSource: "nomad",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.Equal(r, expectUp, out.ServiceTopology.UpstreamDecisions)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("web", func(t *testing.T) {
|
t.Run("web", func(t *testing.T) {
|
||||||
|
retry.Run(t, func(r *retry.R) {
|
||||||
args := structs.ServiceSpecificRequest{
|
args := structs.ServiceSpecificRequest{
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
ServiceName: "web",
|
ServiceName: "web",
|
||||||
}
|
}
|
||||||
var out structs.IndexedServiceTopology
|
var out structs.IndexedServiceTopology
|
||||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.ServiceTopology", &args, &out))
|
require.NoError(r, msgpackrpc.CallWithCodec(codec, "Internal.ServiceTopology", &args, &out))
|
||||||
require.False(t, out.FilteredByACLs)
|
require.False(r, out.FilteredByACLs)
|
||||||
|
|
||||||
// foo/api, foo/api-proxy
|
// foo/api, foo/api-proxy
|
||||||
require.Len(t, out.ServiceTopology.Upstreams, 2)
|
require.Len(r, out.ServiceTopology.Downstreams, 2)
|
||||||
|
|
||||||
|
expectDown := map[string]structs.IntentionDecisionSummary{
|
||||||
|
api.String(): {
|
||||||
|
Allowed: false,
|
||||||
|
HasPermissions: false,
|
||||||
|
ExternalSource: "nomad",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.Equal(r, expectDown, out.ServiceTopology.DownstreamDecisions)
|
||||||
|
|
||||||
// zip/redis, zip/redis-proxy
|
// zip/redis, zip/redis-proxy
|
||||||
require.Len(t, out.ServiceTopology.Downstreams, 2)
|
require.Len(r, out.ServiceTopology.Upstreams, 2)
|
||||||
|
|
||||||
|
expectUp := map[string]structs.IntentionDecisionSummary{
|
||||||
|
redis.String(): {
|
||||||
|
Allowed: false,
|
||||||
|
HasPermissions: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.Equal(r, expectUp, out.ServiceTopology.UpstreamDecisions)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("redis", func(t *testing.T) {
|
t.Run("redis", func(t *testing.T) {
|
||||||
|
retry.Run(t, func(r *retry.R) {
|
||||||
args := structs.ServiceSpecificRequest{
|
args := structs.ServiceSpecificRequest{
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
ServiceName: "redis",
|
ServiceName: "redis",
|
||||||
}
|
}
|
||||||
var out structs.IndexedServiceTopology
|
var out structs.IndexedServiceTopology
|
||||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.ServiceTopology", &args, &out))
|
require.NoError(r, msgpackrpc.CallWithCodec(codec, "Internal.ServiceTopology", &args, &out))
|
||||||
require.False(t, out.FilteredByACLs)
|
require.False(r, out.FilteredByACLs)
|
||||||
|
|
||||||
require.Len(t, out.ServiceTopology.Upstreams, 0)
|
require.Len(r, out.ServiceTopology.Upstreams, 0)
|
||||||
|
|
||||||
// bar/web, bar/web-proxy, baz/web, baz/web-proxy
|
// bar/web, bar/web-proxy, baz/web, baz/web-proxy
|
||||||
require.Len(t, out.ServiceTopology.Downstreams, 4)
|
require.Len(r, out.ServiceTopology.Downstreams, 4)
|
||||||
|
|
||||||
|
expectDown := map[string]structs.IntentionDecisionSummary{
|
||||||
|
web.String(): {
|
||||||
|
Allowed: false,
|
||||||
|
HasPermissions: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.Equal(r, expectDown, out.ServiceTopology.DownstreamDecisions)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/acl"
|
||||||
|
"github.com/hashicorp/consul/agent/connect"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
"github.com/hashicorp/consul/api"
|
"github.com/hashicorp/consul/api"
|
||||||
"github.com/hashicorp/consul/lib"
|
"github.com/hashicorp/consul/lib"
|
||||||
|
@ -2911,6 +2913,7 @@ func checkProtocolMatch(tx ReadTxn, ws memdb.WatchSet, svc *structs.GatewayServi
|
||||||
func (s *Store) ServiceTopology(
|
func (s *Store) ServiceTopology(
|
||||||
ws memdb.WatchSet,
|
ws memdb.WatchSet,
|
||||||
dc, service string,
|
dc, service string,
|
||||||
|
defaultAllow acl.EnforcementDecision,
|
||||||
entMeta *structs.EnterpriseMeta,
|
entMeta *structs.EnterpriseMeta,
|
||||||
) (uint64, *structs.ServiceTopology, error) {
|
) (uint64, *structs.ServiceTopology, error) {
|
||||||
tx := s.db.ReadTxn()
|
tx := s.db.ReadTxn()
|
||||||
|
@ -2936,6 +2939,22 @@ func (s *Store) ServiceTopology(
|
||||||
maxIdx = idx
|
maxIdx = idx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
upstreamDecisions := make(map[string]structs.IntentionDecisionSummary)
|
||||||
|
|
||||||
|
// The given service is the source relative to upstreams
|
||||||
|
sourceURI := connect.SpiffeIDService{
|
||||||
|
Namespace: entMeta.NamespaceOrDefault(),
|
||||||
|
Service: service,
|
||||||
|
}
|
||||||
|
for _, un := range upstreamNames {
|
||||||
|
decision, err := s.IntentionDecision(&sourceURI, un.Name, un.NamespaceOrDefault(), defaultAllow)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, fmt.Errorf("failed to get intention decision from (%s/%s) to (%s/%s): %v",
|
||||||
|
sourceURI.Namespace, sourceURI.Service, un.Name, un.NamespaceOrDefault(), err)
|
||||||
|
}
|
||||||
|
upstreamDecisions[un.String()] = decision
|
||||||
|
}
|
||||||
|
|
||||||
idx, downstreamNames, err := s.downstreamsForServiceTxn(tx, ws, dc, sn)
|
idx, downstreamNames, err := s.downstreamsForServiceTxn(tx, ws, dc, sn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
|
@ -2951,9 +2970,26 @@ func (s *Store) ServiceTopology(
|
||||||
maxIdx = idx
|
maxIdx = idx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downstreamDecisions := make(map[string]structs.IntentionDecisionSummary)
|
||||||
|
for _, dn := range downstreamNames {
|
||||||
|
// Downstreams are the source relative to the given service
|
||||||
|
sourceURI := connect.SpiffeIDService{
|
||||||
|
Namespace: dn.NamespaceOrDefault(),
|
||||||
|
Service: dn.Name,
|
||||||
|
}
|
||||||
|
decision, err := s.IntentionDecision(&sourceURI, service, entMeta.NamespaceOrDefault(), defaultAllow)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, fmt.Errorf("failed to get intention decision from (%s/%s) to (%s/%s): %v",
|
||||||
|
sourceURI.Namespace, sourceURI.Service, service, dn.NamespaceOrDefault(), err)
|
||||||
|
}
|
||||||
|
downstreamDecisions[dn.String()] = decision
|
||||||
|
}
|
||||||
|
|
||||||
resp := &structs.ServiceTopology{
|
resp := &structs.ServiceTopology{
|
||||||
Upstreams: upstreams,
|
Upstreams: upstreams,
|
||||||
Downstreams: downstreams,
|
Downstreams: downstreams,
|
||||||
|
UpstreamDecisions: upstreamDecisions,
|
||||||
|
DownstreamDecisions: downstreamDecisions,
|
||||||
}
|
}
|
||||||
return maxIdx, resp, nil
|
return maxIdx, resp, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/acl"
|
||||||
|
"github.com/hashicorp/consul/agent/connect"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
"github.com/hashicorp/go-memdb"
|
"github.com/hashicorp/go-memdb"
|
||||||
)
|
)
|
||||||
|
@ -447,6 +449,60 @@ func (s *Store) LegacyIntentionDeleteAll(idx uint64) error {
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IntentionDecision returns whether a connection should be allowed from a source URI to some destination
|
||||||
|
// It returns true or false for the enforcement, and also a boolean for whether
|
||||||
|
func (s *Store) IntentionDecision(
|
||||||
|
srcURI connect.CertURI, dstName, dstNS string, defaultDecision acl.EnforcementDecision,
|
||||||
|
) (structs.IntentionDecisionSummary, error) {
|
||||||
|
|
||||||
|
_, matches, err := s.IntentionMatch(nil, &structs.IntentionQueryMatch{
|
||||||
|
Type: structs.IntentionMatchDestination,
|
||||||
|
Entries: []structs.IntentionMatchEntry{
|
||||||
|
{
|
||||||
|
Namespace: dstNS,
|
||||||
|
Name: dstName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return structs.IntentionDecisionSummary{}, err
|
||||||
|
}
|
||||||
|
if len(matches) != 1 {
|
||||||
|
// This should never happen since the documented behavior of the
|
||||||
|
// Match call is that it'll always return exactly the number of results
|
||||||
|
// as entries passed in. But we guard against misbehavior.
|
||||||
|
return structs.IntentionDecisionSummary{}, errors.New("internal error loading matches")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Figure out which source matches this request.
|
||||||
|
var ixnMatch *structs.Intention
|
||||||
|
for _, ixn := range matches[0] {
|
||||||
|
if _, ok := srcURI.Authorize(ixn); ok {
|
||||||
|
ixnMatch = ixn
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp structs.IntentionDecisionSummary
|
||||||
|
if ixnMatch == nil {
|
||||||
|
// No intention found, fall back to default
|
||||||
|
resp.Allowed = defaultDecision == acl.Allow
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intention found, combine action + permissions
|
||||||
|
resp.Allowed = ixnMatch.Action == structs.IntentionActionAllow
|
||||||
|
if len(ixnMatch.Permissions) > 0 {
|
||||||
|
// If there are L7 permissions, DENY.
|
||||||
|
// We are only evaluating source and destination, not the request that will be sent.
|
||||||
|
resp.Allowed = false
|
||||||
|
resp.HasPermissions = true
|
||||||
|
}
|
||||||
|
resp.ExternalSource = ixnMatch.Meta[structs.MetaExternalSource]
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
// IntentionMatch returns the list of intentions that match the namespace and
|
// IntentionMatch returns the list of intentions that match the namespace and
|
||||||
// name for either a source or destination. This applies the resolution rules
|
// name for either a source or destination. This applies the resolution rules
|
||||||
// so wildcards will match any value.
|
// so wildcards will match any value.
|
||||||
|
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/acl"
|
||||||
|
"github.com/hashicorp/consul/agent/connect"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
"github.com/hashicorp/consul/sdk/testutil"
|
"github.com/hashicorp/consul/sdk/testutil"
|
||||||
"github.com/hashicorp/go-memdb"
|
"github.com/hashicorp/go-memdb"
|
||||||
|
@ -1083,6 +1085,123 @@ func TestStore_LegacyIntention_Snapshot_Restore(t *testing.T) {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: This test does not have an equivalent with legacy intentions as an input.
|
||||||
|
// That's because the config vs legacy split is handled by store.IntentionMatch
|
||||||
|
// which has its own tests
|
||||||
|
func TestStore_IntentionDecision(t *testing.T) {
|
||||||
|
// web to redis allowed and with permissions
|
||||||
|
// api to redis denied and without perms (so redis has multiple matches as destination)
|
||||||
|
// api to web without permissions and with meta
|
||||||
|
entries := []structs.ConfigEntry{
|
||||||
|
&structs.ProxyConfigEntry{
|
||||||
|
Kind: structs.ProxyDefaults,
|
||||||
|
Name: structs.ProxyConfigGlobal,
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"protocol": "http",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&structs.ServiceIntentionsConfigEntry{
|
||||||
|
Kind: structs.ServiceIntentions,
|
||||||
|
Name: "redis",
|
||||||
|
Sources: []*structs.SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "web",
|
||||||
|
Permissions: []*structs.IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: structs.IntentionActionAllow,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
Methods: []string{"GET"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "api",
|
||||||
|
Action: structs.IntentionActionDeny,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&structs.ServiceIntentionsConfigEntry{
|
||||||
|
Kind: structs.ServiceIntentions,
|
||||||
|
Name: "web",
|
||||||
|
Meta: map[string]string{structs.MetaExternalSource: "nomad"},
|
||||||
|
Sources: []*structs.SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "api",
|
||||||
|
Action: structs.IntentionActionAllow,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
s := testConfigStateStore(t)
|
||||||
|
for _, entry := range entries {
|
||||||
|
require.NoError(t, s.EnsureConfigEntry(1, entry, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
tt := []struct {
|
||||||
|
name string
|
||||||
|
src string
|
||||||
|
dst string
|
||||||
|
defaultDecision acl.EnforcementDecision
|
||||||
|
expect structs.IntentionDecisionSummary
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no matching intention and default deny",
|
||||||
|
src: "does-not-exist",
|
||||||
|
dst: "ditto",
|
||||||
|
defaultDecision: acl.Deny,
|
||||||
|
expect: structs.IntentionDecisionSummary{Allowed: false},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no matching intention and default allow",
|
||||||
|
src: "does-not-exist",
|
||||||
|
dst: "ditto",
|
||||||
|
defaultDecision: acl.Allow,
|
||||||
|
expect: structs.IntentionDecisionSummary{Allowed: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "denied with permissions",
|
||||||
|
src: "web",
|
||||||
|
dst: "redis",
|
||||||
|
expect: structs.IntentionDecisionSummary{
|
||||||
|
Allowed: false,
|
||||||
|
HasPermissions: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "denied without permissions",
|
||||||
|
src: "api",
|
||||||
|
dst: "redis",
|
||||||
|
expect: structs.IntentionDecisionSummary{
|
||||||
|
Allowed: false,
|
||||||
|
HasPermissions: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "allowed from external source",
|
||||||
|
src: "api",
|
||||||
|
dst: "web",
|
||||||
|
expect: structs.IntentionDecisionSummary{
|
||||||
|
Allowed: true,
|
||||||
|
HasPermissions: false,
|
||||||
|
ExternalSource: "nomad",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tt {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
uri := connect.SpiffeIDService{
|
||||||
|
Service: tc.src,
|
||||||
|
Namespace: structs.IntentionDefaultNamespace,
|
||||||
|
}
|
||||||
|
decision, err := s.IntentionDecision(&uri, tc.dst, structs.IntentionDefaultNamespace, tc.defaultDecision)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.expect, decision)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func disableLegacyIntentions(s *Store) error {
|
func disableLegacyIntentions(s *Store) error {
|
||||||
return s.SystemMetadataSet(1, &structs.SystemMetadataEntry{
|
return s.SystemMetadataSet(1, &structs.SystemMetadataEntry{
|
||||||
Key: structs.SystemMetadataIntentionFormatKey,
|
Key: structs.SystemMetadataIntentionFormatKey,
|
||||||
|
|
|
@ -641,6 +641,17 @@ type IntentionQueryCheckResponse struct {
|
||||||
Allowed bool
|
Allowed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IntentionDecisionSummary contains a summary of a set of intentions between two services
|
||||||
|
// Currently contains:
|
||||||
|
// - Whether all actions are allowed
|
||||||
|
// - Whether the matching intention has L7 permissions attached
|
||||||
|
// - Whether the intention is managed by an external source like k8s,
|
||||||
|
type IntentionDecisionSummary struct {
|
||||||
|
Allowed bool
|
||||||
|
HasPermissions bool
|
||||||
|
ExternalSource string
|
||||||
|
}
|
||||||
|
|
||||||
// IntentionQueryExact holds the parameters for performing a lookup of an
|
// IntentionQueryExact holds the parameters for performing a lookup of an
|
||||||
// intention by its unique name instead of its ID.
|
// intention by its unique name instead of its ID.
|
||||||
type IntentionQueryExact struct {
|
type IntentionQueryExact struct {
|
||||||
|
|
|
@ -104,6 +104,9 @@ const (
|
||||||
// mesh gateway is usable for wan federation.
|
// mesh gateway is usable for wan federation.
|
||||||
MetaWANFederationKey = "consul-wan-federation"
|
MetaWANFederationKey = "consul-wan-federation"
|
||||||
|
|
||||||
|
// MetaExternalSource is the metadata key used when a resource is managed by a source outside Consul like nomad/k8s
|
||||||
|
MetaExternalSource = "external-source"
|
||||||
|
|
||||||
// MaxLockDelay provides a maximum LockDelay value for
|
// MaxLockDelay provides a maximum LockDelay value for
|
||||||
// a session. Any value above this will not be respected.
|
// a session. Any value above this will not be respected.
|
||||||
MaxLockDelay = 60 * time.Second
|
MaxLockDelay = 60 * time.Second
|
||||||
|
@ -1867,6 +1870,9 @@ type IndexedServiceTopology struct {
|
||||||
type ServiceTopology struct {
|
type ServiceTopology struct {
|
||||||
Upstreams CheckServiceNodes
|
Upstreams CheckServiceNodes
|
||||||
Downstreams CheckServiceNodes
|
Downstreams CheckServiceNodes
|
||||||
|
|
||||||
|
UpstreamDecisions map[string]IntentionDecisionSummary
|
||||||
|
DownstreamDecisions map[string]IntentionDecisionSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
// IndexedConfigEntries has its own encoding logic which differs from
|
// IndexedConfigEntries has its own encoding logic which differs from
|
||||||
|
|
|
@ -11,11 +11,6 @@ import (
|
||||||
"github.com/hashicorp/consul/api"
|
"github.com/hashicorp/consul/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// metaExternalSource is the key name for the service instance meta that
|
|
||||||
// defines the external syncing source. This is used by the UI APIs below
|
|
||||||
// to extract this.
|
|
||||||
const metaExternalSource = "external-source"
|
|
||||||
|
|
||||||
// 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"`
|
||||||
|
@ -42,13 +37,6 @@ func (s *ServiceSummary) LessThan(other *ServiceSummary) bool {
|
||||||
return s.Name < other.Name
|
return s.Name < other.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServiceListingSummary struct {
|
|
||||||
ServiceSummary
|
|
||||||
|
|
||||||
ConnectedWithProxy bool
|
|
||||||
ConnectedWithGateway bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type GatewayConfig struct {
|
type GatewayConfig struct {
|
||||||
AssociatedServiceCount int `json:",omitempty"`
|
AssociatedServiceCount int `json:",omitempty"`
|
||||||
Addresses []string `json:",omitempty"`
|
Addresses []string `json:",omitempty"`
|
||||||
|
@ -57,9 +45,22 @@ type GatewayConfig struct {
|
||||||
addressesSet map[string]struct{}
|
addressesSet map[string]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ServiceListingSummary struct {
|
||||||
|
ServiceSummary
|
||||||
|
|
||||||
|
ConnectedWithProxy bool
|
||||||
|
ConnectedWithGateway bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceTopologySummary struct {
|
||||||
|
ServiceSummary
|
||||||
|
|
||||||
|
Intention structs.IntentionDecisionSummary
|
||||||
|
}
|
||||||
|
|
||||||
type ServiceTopology struct {
|
type ServiceTopology struct {
|
||||||
Upstreams []*ServiceSummary
|
Upstreams []*ServiceTopologySummary
|
||||||
Downstreams []*ServiceSummary
|
Downstreams []*ServiceTopologySummary
|
||||||
FilteredByACLs bool
|
FilteredByACLs bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -291,12 +292,38 @@ RPC:
|
||||||
upstreams, _ := summarizeServices(out.ServiceTopology.Upstreams.ToServiceDump(), nil, "")
|
upstreams, _ := summarizeServices(out.ServiceTopology.Upstreams.ToServiceDump(), nil, "")
|
||||||
downstreams, _ := summarizeServices(out.ServiceTopology.Downstreams.ToServiceDump(), nil, "")
|
downstreams, _ := summarizeServices(out.ServiceTopology.Downstreams.ToServiceDump(), nil, "")
|
||||||
|
|
||||||
sum := ServiceTopology{
|
var (
|
||||||
Upstreams: prepSummaryOutput(upstreams, true),
|
upstreamResp []*ServiceTopologySummary
|
||||||
Downstreams: prepSummaryOutput(downstreams, true),
|
downstreamResp []*ServiceTopologySummary
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sort and attach intention data for upstreams and downstreams
|
||||||
|
sortedUpstreams := prepSummaryOutput(upstreams, true)
|
||||||
|
for _, svc := range sortedUpstreams {
|
||||||
|
sn := structs.NewServiceName(svc.Name, &svc.EnterpriseMeta)
|
||||||
|
sum := ServiceTopologySummary{
|
||||||
|
ServiceSummary: *svc,
|
||||||
|
Intention: out.ServiceTopology.UpstreamDecisions[sn.String()],
|
||||||
|
}
|
||||||
|
upstreamResp = append(upstreamResp, &sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
sortedDownstreams := prepSummaryOutput(downstreams, true)
|
||||||
|
for _, svc := range sortedDownstreams {
|
||||||
|
sn := structs.NewServiceName(svc.Name, &svc.EnterpriseMeta)
|
||||||
|
sum := ServiceTopologySummary{
|
||||||
|
ServiceSummary: *svc,
|
||||||
|
Intention: out.ServiceTopology.DownstreamDecisions[sn.String()],
|
||||||
|
}
|
||||||
|
downstreamResp = append(downstreamResp, &sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
topo := ServiceTopology{
|
||||||
|
Upstreams: upstreamResp,
|
||||||
|
Downstreams: downstreamResp,
|
||||||
FilteredByACLs: out.FilteredByACLs,
|
FilteredByACLs: out.FilteredByACLs,
|
||||||
}
|
}
|
||||||
return sum, nil
|
return topo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func summarizeServices(dump structs.ServiceDump, cfg *config.RuntimeConfig, dc string) (map[structs.ServiceName]*ServiceSummary, map[structs.ServiceName]bool) {
|
func summarizeServices(dump structs.ServiceDump, cfg *config.RuntimeConfig, dc string) (map[structs.ServiceName]*ServiceSummary, map[structs.ServiceName]bool) {
|
||||||
|
@ -370,8 +397,8 @@ func summarizeServices(dump structs.ServiceDump, cfg *config.RuntimeConfig, dc s
|
||||||
// sources. We only want to add unique sources so there is extra
|
// sources. We only want to add unique sources so there is extra
|
||||||
// accounting here with an unexported field to maintain the set
|
// accounting here with an unexported field to maintain the set
|
||||||
// of sources.
|
// of sources.
|
||||||
if len(svc.Meta) > 0 && svc.Meta[metaExternalSource] != "" {
|
if len(svc.Meta) > 0 && svc.Meta[structs.MetaExternalSource] != "" {
|
||||||
source := svc.Meta[metaExternalSource]
|
source := svc.Meta[structs.MetaExternalSource]
|
||||||
if sum.externalSourceSet == nil {
|
if sum.externalSourceSet == nil {
|
||||||
sum.externalSourceSet = make(map[string]struct{})
|
sum.externalSourceSet = make(map[string]struct{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package agent
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -246,7 +247,7 @@ func TestUiServices(t *testing.T) {
|
||||||
Service: "api-proxy",
|
Service: "api-proxy",
|
||||||
ID: "api-proxy-1",
|
ID: "api-proxy-1",
|
||||||
Tags: []string{},
|
Tags: []string{},
|
||||||
Meta: map[string]string{metaExternalSource: "k8s"},
|
Meta: map[string]string{structs.MetaExternalSource: "k8s"},
|
||||||
Port: 1234,
|
Port: 1234,
|
||||||
Proxy: structs.ConnectProxyConfig{
|
Proxy: structs.ConnectProxyConfig{
|
||||||
DestinationServiceName: "api",
|
DestinationServiceName: "api",
|
||||||
|
@ -272,7 +273,7 @@ func TestUiServices(t *testing.T) {
|
||||||
Service: "web",
|
Service: "web",
|
||||||
ID: "web-1",
|
ID: "web-1",
|
||||||
Tags: []string{},
|
Tags: []string{},
|
||||||
Meta: map[string]string{metaExternalSource: "k8s"},
|
Meta: map[string]string{structs.MetaExternalSource: "k8s"},
|
||||||
Port: 1234,
|
Port: 1234,
|
||||||
},
|
},
|
||||||
Checks: []*structs.HealthCheck{
|
Checks: []*structs.HealthCheck{
|
||||||
|
@ -936,7 +937,7 @@ func TestUIServiceTopology(t *testing.T) {
|
||||||
a := NewTestAgent(t, "")
|
a := NewTestAgent(t, "")
|
||||||
defer a.Shutdown()
|
defer a.Shutdown()
|
||||||
|
|
||||||
// Register terminating gateway and config entry linking it to postgres + redis
|
// Register api -> web -> redis
|
||||||
{
|
{
|
||||||
registrations := map[string]*structs.RegisterRequest{
|
registrations := map[string]*structs.RegisterRequest{
|
||||||
"Node foo": {
|
"Node foo": {
|
||||||
|
@ -1204,17 +1205,73 @@ func TestUIServiceTopology(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add intentions: deny all, web -> redis with L7 perms, but omit intention for api -> web
|
||||||
|
{
|
||||||
|
entries := []structs.ConfigEntryRequest{
|
||||||
|
{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Entry: &structs.ProxyConfigEntry{
|
||||||
|
Kind: structs.ProxyDefaults,
|
||||||
|
Name: structs.ProxyConfigGlobal,
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"protocol": "http",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Entry: &structs.ServiceIntentionsConfigEntry{
|
||||||
|
Kind: structs.ServiceIntentions,
|
||||||
|
Name: "redis",
|
||||||
|
Sources: []*structs.SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "web",
|
||||||
|
Permissions: []*structs.IntentionPermission{
|
||||||
|
{
|
||||||
|
Action: structs.IntentionActionAllow,
|
||||||
|
HTTP: &structs.IntentionHTTPPermission{
|
||||||
|
Methods: []string{"GET"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Entry: &structs.ServiceIntentionsConfigEntry{
|
||||||
|
Kind: structs.ServiceIntentions,
|
||||||
|
Name: "*",
|
||||||
|
Meta: map[string]string{structs.MetaExternalSource: "nomad"},
|
||||||
|
Sources: []*structs.SourceIntention{
|
||||||
|
{
|
||||||
|
Name: "*",
|
||||||
|
Action: structs.IntentionActionDeny,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, req := range entries {
|
||||||
|
out := false
|
||||||
|
require.NoError(t, a.RPC("ConfigEntry.Apply", &req, &out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
t.Run("api", func(t *testing.T) {
|
t.Run("api", func(t *testing.T) {
|
||||||
|
retry.Run(t, func(r *retry.R) {
|
||||||
// Request topology for api
|
// Request topology for api
|
||||||
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/api", nil)
|
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/api", nil)
|
||||||
resp := httptest.NewRecorder()
|
resp := httptest.NewRecorder()
|
||||||
obj, err := a.srv.UIServiceTopology(resp, req)
|
obj, err := a.srv.UIServiceTopology(resp, req)
|
||||||
assert.Nil(t, err)
|
assert.Nil(r, err)
|
||||||
assertIndex(t, resp)
|
require.NoError(r, checkIndex(resp))
|
||||||
|
|
||||||
expect := ServiceTopology{
|
expect := ServiceTopology{
|
||||||
Upstreams: []*ServiceSummary{
|
Upstreams: []*ServiceTopologySummary{
|
||||||
{
|
{
|
||||||
|
ServiceSummary: ServiceSummary{
|
||||||
Name: "web",
|
Name: "web",
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
Nodes: []string{"bar", "baz"},
|
Nodes: []string{"bar", "baz"},
|
||||||
|
@ -1224,6 +1281,12 @@ func TestUIServiceTopology(t *testing.T) {
|
||||||
ChecksCritical: 2,
|
ChecksCritical: 2,
|
||||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||||
},
|
},
|
||||||
|
Intention: structs.IntentionDecisionSummary{
|
||||||
|
Allowed: false,
|
||||||
|
HasPermissions: false,
|
||||||
|
ExternalSource: "nomad",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
FilteredByACLs: false,
|
FilteredByACLs: false,
|
||||||
}
|
}
|
||||||
|
@ -1234,20 +1297,23 @@ func TestUIServiceTopology(t *testing.T) {
|
||||||
u.externalSourceSet = nil
|
u.externalSourceSet = nil
|
||||||
u.checks = nil
|
u.checks = nil
|
||||||
}
|
}
|
||||||
require.Equal(t, expect, result)
|
require.Equal(r, expect, result)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("web", func(t *testing.T) {
|
t.Run("web", func(t *testing.T) {
|
||||||
|
retry.Run(t, func(r *retry.R) {
|
||||||
// Request topology for web
|
// Request topology for web
|
||||||
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/web", nil)
|
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/web", nil)
|
||||||
resp := httptest.NewRecorder()
|
resp := httptest.NewRecorder()
|
||||||
obj, err := a.srv.UIServiceTopology(resp, req)
|
obj, err := a.srv.UIServiceTopology(resp, req)
|
||||||
assert.Nil(t, err)
|
assert.Nil(r, err)
|
||||||
assertIndex(t, resp)
|
require.NoError(r, checkIndex(resp))
|
||||||
|
|
||||||
expect := ServiceTopology{
|
expect := ServiceTopology{
|
||||||
Upstreams: []*ServiceSummary{
|
Upstreams: []*ServiceTopologySummary{
|
||||||
{
|
{
|
||||||
|
ServiceSummary: ServiceSummary{
|
||||||
Name: "redis",
|
Name: "redis",
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
Nodes: []string{"zip"},
|
Nodes: []string{"zip"},
|
||||||
|
@ -1256,9 +1322,15 @@ func TestUIServiceTopology(t *testing.T) {
|
||||||
ChecksCritical: 1,
|
ChecksCritical: 1,
|
||||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||||
},
|
},
|
||||||
|
Intention: structs.IntentionDecisionSummary{
|
||||||
|
Allowed: false,
|
||||||
|
HasPermissions: true,
|
||||||
},
|
},
|
||||||
Downstreams: []*ServiceSummary{
|
},
|
||||||
|
},
|
||||||
|
Downstreams: []*ServiceTopologySummary{
|
||||||
{
|
{
|
||||||
|
ServiceSummary: ServiceSummary{
|
||||||
Name: "api",
|
Name: "api",
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
Nodes: []string{"foo"},
|
Nodes: []string{"foo"},
|
||||||
|
@ -1266,6 +1338,12 @@ func TestUIServiceTopology(t *testing.T) {
|
||||||
ChecksPassing: 3,
|
ChecksPassing: 3,
|
||||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||||
},
|
},
|
||||||
|
Intention: structs.IntentionDecisionSummary{
|
||||||
|
Allowed: false,
|
||||||
|
HasPermissions: false,
|
||||||
|
ExternalSource: "nomad",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
FilteredByACLs: false,
|
FilteredByACLs: false,
|
||||||
}
|
}
|
||||||
|
@ -1280,20 +1358,23 @@ func TestUIServiceTopology(t *testing.T) {
|
||||||
d.externalSourceSet = nil
|
d.externalSourceSet = nil
|
||||||
d.checks = nil
|
d.checks = nil
|
||||||
}
|
}
|
||||||
require.Equal(t, expect, result)
|
require.Equal(r, expect, result)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("redis", func(t *testing.T) {
|
t.Run("redis", func(t *testing.T) {
|
||||||
|
retry.Run(t, func(r *retry.R) {
|
||||||
// Request topology for redis
|
// Request topology for redis
|
||||||
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/redis", nil)
|
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/redis", nil)
|
||||||
resp := httptest.NewRecorder()
|
resp := httptest.NewRecorder()
|
||||||
obj, err := a.srv.UIServiceTopology(resp, req)
|
obj, err := a.srv.UIServiceTopology(resp, req)
|
||||||
assert.Nil(t, err)
|
assert.Nil(r, err)
|
||||||
assertIndex(t, resp)
|
require.NoError(r, checkIndex(resp))
|
||||||
|
|
||||||
expect := ServiceTopology{
|
expect := ServiceTopology{
|
||||||
Downstreams: []*ServiceSummary{
|
Downstreams: []*ServiceTopologySummary{
|
||||||
{
|
{
|
||||||
|
ServiceSummary: ServiceSummary{
|
||||||
Name: "web",
|
Name: "web",
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
Nodes: []string{"bar", "baz"},
|
Nodes: []string{"bar", "baz"},
|
||||||
|
@ -1303,6 +1384,11 @@ func TestUIServiceTopology(t *testing.T) {
|
||||||
ChecksCritical: 2,
|
ChecksCritical: 2,
|
||||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||||
},
|
},
|
||||||
|
Intention: structs.IntentionDecisionSummary{
|
||||||
|
Allowed: false,
|
||||||
|
HasPermissions: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
FilteredByACLs: false,
|
FilteredByACLs: false,
|
||||||
}
|
}
|
||||||
|
@ -1313,6 +1399,7 @@ func TestUIServiceTopology(t *testing.T) {
|
||||||
d.externalSourceSet = nil
|
d.externalSourceSet = nil
|
||||||
d.checks = nil
|
d.checks = nil
|
||||||
}
|
}
|
||||||
require.Equal(t, expect, result)
|
require.Equal(r, expect, result)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue