Merge pull request #10016 from hashicorp/topology-update

This commit is contained in:
Freddy 2021-04-15 14:11:23 -06:00 committed by GitHub
commit 3be304be16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 588 additions and 143 deletions

3
.changelog/10016.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
connect: Update the service mesh visualization to account for transparent proxies.
```

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) {
t.Helper()
// api and api-proxy on node foo - upstream: web
// ingress-gateway on node edge - upstream: api
// api and api-proxy on node foo - transparent proxy
// web and web-proxy on node bar - upstream: redis
// web and web-proxy on node baz - upstream: redis
// web and web-proxy on node baz - transparent proxy
// redis and redis-proxy on node zip
registrations := map[string]*structs.RegisterRequest{
"Node edge": {
Datacenter: "dc1",
Node: "edge",
ID: types.NodeID("8e3481c0-760e-4b5f-a3b8-6c8c559e8a15"),
Address: "127.0.0.1",
Checks: structs.HealthChecks{
&structs.HealthCheck{
Node: "edge",
CheckID: "edge:alive",
Name: "edge-liveness",
Status: api.HealthPassing,
},
},
WriteRequest: structs.WriteRequest{Token: token},
},
"Service ingress on edge": {
Datacenter: "dc1",
Node: "edge",
SkipNodeUpdate: true,
Service: &structs.NodeService{
Kind: structs.ServiceKindIngressGateway,
ID: "ingress",
Service: "ingress",
Port: 8443,
Address: "198.18.1.1",
},
Checks: structs.HealthChecks{
&structs.HealthCheck{
Node: "edge",
CheckID: "edge:ingress",
Name: "ingress-liveness",
Status: api.HealthPassing,
ServiceID: "ingress",
ServiceName: "ingress",
},
},
WriteRequest: structs.WriteRequest{Token: token},
},
"Node foo": {
Datacenter: "dc1",
Node: "foo",
@ -627,13 +666,8 @@ func registerTestTopologyEntries(t *testing.T, codec rpc.ClientCodec, token stri
Port: 8443,
Address: "198.18.1.2",
Proxy: structs.ConnectProxyConfig{
Mode: structs.ProxyModeTransparent,
DestinationServiceName: "api",
Upstreams: structs.Upstreams{
{
DestinationName: "web",
LocalBindPort: 8080,
},
},
},
},
Checks: structs.HealthChecks{
@ -767,13 +801,8 @@ func registerTestTopologyEntries(t *testing.T, codec rpc.ClientCodec, token stri
Port: 8443,
Address: "198.18.1.40",
Proxy: structs.ConnectProxyConfig{
Mode: structs.ProxyModeTransparent,
DestinationServiceName: "web",
Upstreams: structs.Upstreams{
{
DestinationName: "redis",
LocalBindPort: 123,
},
},
},
},
Checks: structs.HealthChecks{
@ -855,7 +884,10 @@ func registerTestTopologyEntries(t *testing.T, codec rpc.ClientCodec, token stri
}
registerTestCatalogEntriesMap(t, codec, registrations)
// Add intentions: deny all, web -> redis with L7 perms, but omit intention for api -> web
// ingress -> api gateway config entry (but no intention)
// wildcard deny intention
// api -> web exact intention
// web -> redis exact intention
entries := []structs.ConfigEntryRequest{
{
Datacenter: "dc1",
@ -868,6 +900,39 @@ func registerTestTopologyEntries(t *testing.T, codec rpc.ClientCodec, token stri
},
WriteRequest: structs.WriteRequest{Token: token},
},
{
Datacenter: "dc1",
Entry: &structs.IngressGatewayConfigEntry{
Kind: structs.IngressGateway,
Name: "ingress",
Listeners: []structs.IngressListener{
{
Port: 8443,
Protocol: "http",
Services: []structs.IngressService{
{
Name: "api",
},
},
},
},
},
WriteRequest: structs.WriteRequest{Token: token},
},
{
Datacenter: "dc1",
Entry: &structs.ServiceIntentionsConfigEntry{
Kind: structs.ServiceIntentions,
Name: "web",
Sources: []*structs.SourceIntention{
{
Action: structs.IntentionActionAllow,
Name: "api",
},
},
},
WriteRequest: structs.WriteRequest{Token: token},
},
{
Datacenter: "dc1",
Entry: &structs.ServiceIntentionsConfigEntry{

View File

@ -1690,20 +1690,67 @@ func TestInternal_ServiceTopology(t *testing.T) {
codec := rpcClient(t, s1)
defer codec.Close()
// api and api-proxy on node foo - upstream: web
// web and web-proxy on node bar - upstream: redis
// web and web-proxy on node baz - upstream: redis
// redis and redis-proxy on node zip
// wildcard deny intention
// web -> redis exact intentino
// ingress-gateway on node edge - upstream: api
// ingress -> api gateway config entry (but no intention)
// api and api-proxy on node foo - transparent proxy
// api -> web exact intention
// web and web-proxy on node bar - upstream: redis
// web and web-proxy on node baz - transparent proxy
// web -> redis exact intention
// redis and redis-proxy on node zip
registerTestTopologyEntries(t, codec, "")
var (
api = structs.NewServiceName("api", structs.DefaultEnterpriseMeta())
web = structs.NewServiceName("web", structs.DefaultEnterpriseMeta())
redis = structs.NewServiceName("redis", structs.DefaultEnterpriseMeta())
ingress = structs.NewServiceName("ingress", structs.DefaultEnterpriseMeta())
api = structs.NewServiceName("api", structs.DefaultEnterpriseMeta())
web = structs.NewServiceName("web", structs.DefaultEnterpriseMeta())
redis = structs.NewServiceName("redis", structs.DefaultEnterpriseMeta())
)
t.Run("ingress", func(t *testing.T) {
retry.Run(t, func(r *retry.R) {
args := structs.ServiceSpecificRequest{
Datacenter: "dc1",
ServiceName: "ingress",
}
var out structs.IndexedServiceTopology
require.NoError(r, msgpackrpc.CallWithCodec(codec, "Internal.ServiceTopology", &args, &out))
require.False(r, out.FilteredByACLs)
require.Equal(r, "http", out.ServiceTopology.MetricsProtocol)
// foo/api, foo/api-proxy
require.Len(r, out.ServiceTopology.Upstreams, 2)
require.Len(r, out.ServiceTopology.Downstreams, 0)
expectUp := map[string]structs.IntentionDecisionSummary{
api.String(): {
DefaultAllow: true,
Allowed: false,
HasPermissions: false,
ExternalSource: "nomad",
// From wildcard deny
HasExact: false,
},
}
require.Equal(r, expectUp, out.ServiceTopology.UpstreamDecisions)
expectUpstreamSources := map[string]string{
api.String(): structs.TopologySourceRegistration,
}
require.Equal(r, expectUpstreamSources, out.ServiceTopology.UpstreamSources)
require.Empty(r, out.ServiceTopology.DownstreamSources)
// The ingress gateway has an explicit upstream
require.False(r, out.ServiceTopology.TransparentProxy)
})
})
t.Run("api", func(t *testing.T) {
retry.Run(t, func(r *retry.R) {
args := structs.ServiceSpecificRequest{
@ -1715,12 +1762,12 @@ func TestInternal_ServiceTopology(t *testing.T) {
require.False(r, out.FilteredByACLs)
require.Equal(r, "http", out.ServiceTopology.MetricsProtocol)
// bar/web, bar/web-proxy, baz/web, baz/web-proxy
require.Len(r, out.ServiceTopology.Upstreams, 4)
require.Len(r, out.ServiceTopology.Downstreams, 0)
// edge/ingress
require.Len(r, out.ServiceTopology.Downstreams, 1)
expectUp := map[string]structs.IntentionDecisionSummary{
web.String(): {
expectDown := map[string]structs.IntentionDecisionSummary{
ingress.String(): {
DefaultAllow: true,
Allowed: false,
HasPermissions: false,
ExternalSource: "nomad",
@ -1729,7 +1776,33 @@ func TestInternal_ServiceTopology(t *testing.T) {
HasExact: false,
},
}
require.Equal(r, expectDown, out.ServiceTopology.DownstreamDecisions)
expectDownstreamSources := map[string]string{
ingress.String(): structs.TopologySourceRegistration,
}
require.Equal(r, expectDownstreamSources, out.ServiceTopology.DownstreamSources)
// bar/web, bar/web-proxy, baz/web, baz/web-proxy
require.Len(r, out.ServiceTopology.Upstreams, 4)
expectUp := map[string]structs.IntentionDecisionSummary{
web.String(): {
DefaultAllow: true,
Allowed: true,
HasPermissions: false,
HasExact: true,
},
}
require.Equal(r, expectUp, out.ServiceTopology.UpstreamDecisions)
expectUpstreamSources := map[string]string{
web.String(): structs.TopologySourceSpecificIntention,
}
require.Equal(r, expectUpstreamSources, out.ServiceTopology.UpstreamSources)
// The only instance of api's proxy is in transparent mode
require.True(r, out.ServiceTopology.TransparentProxy)
})
})
@ -1749,27 +1822,40 @@ func TestInternal_ServiceTopology(t *testing.T) {
expectDown := map[string]structs.IntentionDecisionSummary{
api.String(): {
Allowed: false,
DefaultAllow: true,
Allowed: true,
HasPermissions: false,
ExternalSource: "nomad",
// From wildcard deny
HasExact: false,
HasExact: true,
},
}
require.Equal(r, expectDown, out.ServiceTopology.DownstreamDecisions)
expectDownstreamSources := map[string]string{
api.String(): structs.TopologySourceSpecificIntention,
}
require.Equal(r, expectDownstreamSources, out.ServiceTopology.DownstreamSources)
// zip/redis, zip/redis-proxy
require.Len(r, out.ServiceTopology.Upstreams, 2)
expectUp := map[string]structs.IntentionDecisionSummary{
redis.String(): {
DefaultAllow: true,
Allowed: false,
HasPermissions: true,
HasExact: true,
},
}
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)
})
})
@ -1791,12 +1877,22 @@ func TestInternal_ServiceTopology(t *testing.T) {
expectDown := map[string]structs.IntentionDecisionSummary{
web.String(): {
DefaultAllow: true,
Allowed: false,
HasPermissions: true,
HasExact: true,
},
}
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)
})
})
}
@ -1821,9 +1917,17 @@ func TestInternal_ServiceTopology_ACL(t *testing.T) {
codec := rpcClient(t, s1)
defer codec.Close()
// api and api-proxy on node foo - upstream: web
// wildcard deny intention
// ingress-gateway on node edge - upstream: api
// ingress -> api gateway config entry (but no intention)
// api and api-proxy on node foo - transparent proxy
// api -> web exact intention
// web and web-proxy on node bar - upstream: redis
// web and web-proxy on node baz - upstream: redis
// web and web-proxy on node baz - transparent proxy
// web -> redis exact intention
// redis and redis-proxy on node zip
registerTestTopologyEntries(t, codec, TestDefaultMasterToken)

View File

@ -2853,6 +2853,8 @@ func checkProtocolMatch(tx ReadTxn, ws memdb.WatchSet, svc *structs.GatewayServi
return idx, svc.Protocol == protocol, nil
}
// TODO(freddy) Split this up. The upstream/downstream logic is very similar.
// TODO(freddy) Add comprehensive state store test
func (s *Store) ServiceTopology(
ws memdb.WatchSet,
dc, service string,
@ -2863,14 +2865,15 @@ func (s *Store) ServiceTopology(
tx := s.db.ReadTxn()
defer tx.Abort()
sn := structs.NewServiceName(service, entMeta)
var (
maxIdx uint64
protocol string
err error
sn = structs.NewServiceName(service, entMeta)
maxIdx uint64
protocol string
err error
fullyTransparent bool
hasTransparent bool
)
switch kind {
case structs.ServiceKindIngressGateway:
maxIdx, protocol, err = metricsProtocolForIngressGateway(tx, ws, sn)
@ -2884,6 +2887,38 @@ func (s *Store) ServiceTopology(
return 0, nil, fmt.Errorf("failed to fetch protocol for service %s: %v", sn.String(), err)
}
// Fetch connect endpoints for the target service in order to learn if its proxies are configured as
// transparent proxies.
if entMeta == nil {
entMeta = structs.DefaultEnterpriseMeta()
}
q := Query{Value: service, EnterpriseMeta: *entMeta}
idx, proxies, err := serviceNodesTxn(tx, ws, indexConnect, q)
if err != nil {
return 0, nil, fmt.Errorf("failed to fetch connect endpoints for service %s: %v", sn.String(), err)
}
if idx > maxIdx {
maxIdx = idx
}
if len(proxies) == 0 {
break
}
fullyTransparent = true
for _, proxy := range proxies {
switch proxy.ServiceProxy.Mode {
case structs.ProxyModeTransparent:
hasTransparent = true
default:
// Only consider the target proxy to be transparent when all instances are in that mode.
// This is done because the flag is used to display warnings about proxies needing to enable
// transparent proxy mode. If ANY instance isn't in the right mode then the warming applies.
fullyTransparent = false
}
}
default:
return 0, nil, fmt.Errorf("unsupported kind %q", kind)
}
@ -2895,7 +2930,48 @@ func (s *Store) ServiceTopology(
if idx > maxIdx {
maxIdx = idx
}
idx, upstreams, err := s.combinedServiceNodesTxn(tx, ws, upstreamNames)
var (
seenUpstreams = make(map[string]struct{})
upstreamSources = make(map[string]string)
)
for _, un := range upstreamNames {
if _, ok := seenUpstreams[un.String()]; !ok {
seenUpstreams[un.String()] = struct{}{}
}
upstreamSources[un.String()] = structs.TopologySourceRegistration
}
idx, intentionUpstreams, err := s.intentionTopologyTxn(tx, ws, sn, false, defaultAllow)
if err != nil {
return 0, nil, err
}
if idx > maxIdx {
maxIdx = idx
}
upstreamDecisions := make(map[string]structs.IntentionDecisionSummary)
for _, svc := range intentionUpstreams {
if _, ok := seenUpstreams[svc.Name.String()]; ok {
// Avoid duplicating entry
continue
}
upstreamDecisions[svc.Name.String()] = svc.Decision
upstreamNames = append(upstreamNames, svc.Name)
var source string
switch {
case svc.Decision.HasExact:
source = structs.TopologySourceSpecificIntention
case svc.Decision.DefaultAllow:
source = structs.TopologySourceDefaultAllow
default:
source = structs.TopologySourceWildcardIntention
}
upstreamSources[svc.Name.String()] = source
}
idx, unfilteredUpstreams, err := s.combinedServiceNodesTxn(tx, ws, upstreamNames)
if err != nil {
return 0, nil, fmt.Errorf("failed to get upstreams for %q: %v", sn.String(), err)
}
@ -2903,14 +2979,32 @@ func (s *Store) ServiceTopology(
maxIdx = idx
}
upstreamDecisions := make(map[string]structs.IntentionDecisionSummary)
var upstreams structs.CheckServiceNodes
for _, upstream := range unfilteredUpstreams {
sn := upstream.Service.CompoundServiceName()
if upstream.Service.Kind == structs.ServiceKindConnectProxy {
sn = structs.NewServiceName(upstream.Service.Proxy.DestinationServiceName, &upstream.Service.EnterpriseMeta)
}
// Avoid returning upstreams from intentions when none of the proxy instances of the target are in transparent mode.
if !hasTransparent && upstreamSources[sn.String()] != structs.TopologySourceRegistration {
continue
}
upstreams = append(upstreams, upstream)
}
matchEntry := structs.IntentionMatchEntry{
Namespace: entMeta.NamespaceOrDefault(),
Name: service,
}
// The given service is a source relative to its upstreams
_, srcIntentions, err := compatIntentionMatchOneTxn(tx, ws, matchEntry, structs.IntentionMatchSource)
_, srcIntentions, err := compatIntentionMatchOneTxn(
tx,
ws,
matchEntry,
// The given service is a source relative to its upstreams
structs.IntentionMatchSource,
)
if err != nil {
return 0, nil, fmt.Errorf("failed to query intentions for %s", sn.String())
}
@ -2930,7 +3024,48 @@ func (s *Store) ServiceTopology(
if idx > maxIdx {
maxIdx = idx
}
idx, downstreams, err := s.combinedServiceNodesTxn(tx, ws, downstreamNames)
var (
seenDownstreams = make(map[string]struct{})
downstreamSources = make(map[string]string)
)
for _, dn := range downstreamNames {
if _, ok := seenDownstreams[dn.String()]; !ok {
seenDownstreams[dn.String()] = struct{}{}
}
downstreamSources[dn.String()] = structs.TopologySourceRegistration
}
idx, intentionDownstreams, err := s.intentionTopologyTxn(tx, ws, sn, true, defaultAllow)
if err != nil {
return 0, nil, err
}
if idx > maxIdx {
maxIdx = idx
}
downstreamDecisions := make(map[string]structs.IntentionDecisionSummary)
for _, svc := range intentionDownstreams {
if _, ok := seenDownstreams[svc.Name.String()]; ok {
// Avoid duplicating entry
continue
}
downstreamNames = append(downstreamNames, svc.Name)
downstreamDecisions[svc.Name.String()] = svc.Decision
var source string
switch {
case svc.Decision.HasExact:
source = structs.TopologySourceSpecificIntention
case svc.Decision.DefaultAllow:
source = structs.TopologySourceDefaultAllow
default:
source = structs.TopologySourceWildcardIntention
}
downstreamSources[svc.Name.String()] = source
}
idx, unfilteredDownstreams, err := s.combinedServiceNodesTxn(tx, ws, downstreamNames)
if err != nil {
return 0, nil, fmt.Errorf("failed to get downstreams for %q: %v", sn.String(), err)
}
@ -2938,12 +3073,39 @@ func (s *Store) ServiceTopology(
maxIdx = idx
}
// The given service is a destination relative to its downstreams
_, dstIntentions, err := compatIntentionMatchOneTxn(tx, ws, matchEntry, structs.IntentionMatchDestination)
// Store downstreams with at least one instance in transparent proxy mode.
// This is to avoid returning downstreams from intentions when none of the downstreams are transparent proxies.
tproxyMap := make(map[structs.ServiceName]struct{})
for _, downstream := range unfilteredDownstreams {
if downstream.Service.Proxy.Mode == structs.ProxyModeTransparent {
sn := structs.NewServiceName(downstream.Service.Proxy.DestinationServiceName, &downstream.Service.EnterpriseMeta)
tproxyMap[sn] = struct{}{}
}
}
var downstreams structs.CheckServiceNodes
for _, downstream := range unfilteredDownstreams {
sn := downstream.Service.CompoundServiceName()
if downstream.Service.Kind == structs.ServiceKindConnectProxy {
sn = structs.NewServiceName(downstream.Service.Proxy.DestinationServiceName, &downstream.Service.EnterpriseMeta)
}
if _, ok := tproxyMap[sn]; !ok && downstreamSources[sn.String()] != structs.TopologySourceRegistration {
continue
}
downstreams = append(downstreams, downstream)
}
_, dstIntentions, err := compatIntentionMatchOneTxn(
tx,
ws,
matchEntry,
// The given service is a destination relative to its downstreams
structs.IntentionMatchDestination,
)
if err != nil {
return 0, nil, fmt.Errorf("failed to query intentions for %s", sn.String())
}
downstreamDecisions := make(map[string]structs.IntentionDecisionSummary)
for _, dn := range downstreamNames {
decision, err := s.IntentionDecision(dn.Name, dn.NamespaceOrDefault(), dstIntentions, structs.IntentionMatchSource, defaultAllow, false)
if err != nil {
@ -2954,11 +3116,14 @@ func (s *Store) ServiceTopology(
}
resp := &structs.ServiceTopology{
TransparentProxy: fullyTransparent,
MetricsProtocol: protocol,
Upstreams: upstreams,
Downstreams: downstreams,
UpstreamDecisions: upstreamDecisions,
DownstreamDecisions: downstreamDecisions,
UpstreamSources: upstreamSources,
DownstreamSources: downstreamSources,
}
return maxIdx, resp, nil
}
@ -2995,7 +3160,6 @@ func (s *Store) combinedServiceNodesTxn(tx ReadTxn, ws memdb.WatchSet, names []s
// downstreamsForServiceTxn will find all downstream services that could route traffic to the input service.
// There are two factors at play. Upstreams defined in a proxy registration, and the discovery chain for those upstreams.
// TODO (freddy): Account for ingress gateways
func (s *Store) downstreamsForServiceTxn(tx ReadTxn, ws memdb.WatchSet, dc string, service structs.ServiceName) (uint64, []structs.ServiceName, error) {
// First fetch services that have discovery chains that eventually route to the target service
idx, sources, err := s.discoveryChainSourcesTxn(tx, ws, dc, service)

View File

@ -750,10 +750,12 @@ func (s *Store) IntentionDecision(
}
}
var resp structs.IntentionDecisionSummary
resp := structs.IntentionDecisionSummary{
DefaultAllow: defaultDecision == acl.Allow,
}
if ixnMatch == nil {
// No intention found, fall back to default
resp.Allowed = defaultDecision == acl.Allow
resp.Allowed = resp.DefaultAllow
return resp, nil
}
@ -931,6 +933,11 @@ func intentionMatchGetParams(entry structs.IntentionMatchEntry) ([][]interface{}
return result, nil
}
type ServiceWithDecision struct {
Name structs.ServiceName
Decision structs.IntentionDecisionSummary
}
// IntentionTopology returns the upstreams or downstreams of a service. Upstreams and downstreams are inferred from
// intentions. If intentions allow a connection from the target to some candidate service, the candidate service is considered
// an upstream of the target.
@ -939,6 +946,25 @@ func (s *Store) IntentionTopology(ws memdb.WatchSet,
tx := s.db.ReadTxn()
defer tx.Abort()
idx, services, err := s.intentionTopologyTxn(tx, ws, target, downstreams, defaultDecision)
if err != nil {
requested := "upstreams"
if downstreams {
requested = "downstreams"
}
return 0, nil, fmt.Errorf("failed to fetch %s for %s: %v", requested, target.String(), err)
}
resp := make(structs.ServiceList, 0)
for _, svc := range services {
resp = append(resp, svc.Name)
}
return idx, resp, nil
}
func (s *Store) intentionTopologyTxn(tx ReadTxn, ws memdb.WatchSet,
target structs.ServiceName, downstreams bool, defaultDecision acl.EnforcementDecision) (uint64, []ServiceWithDecision, error) {
var maxIdx uint64
// If querying the upstreams for a service, we first query intentions that apply to the target service as a source.
@ -997,7 +1023,7 @@ func (s *Store) IntentionTopology(ws memdb.WatchSet,
if downstreams {
decisionMatchType = structs.IntentionMatchSource
}
result := make(structs.ServiceList, 0, len(allServices))
result := make([]ServiceWithDecision, 0, len(allServices))
for _, candidate := range allServices {
if candidate.Name == structs.ConsulServiceName {
continue
@ -1014,7 +1040,11 @@ func (s *Store) IntentionTopology(ws memdb.WatchSet,
if !decision.Allowed || target.Matches(candidate) {
continue
}
result = append(result, candidate)
result = append(result, ServiceWithDecision{
Name: candidate,
Decision: decision,
})
}
return maxIdx, result, err
}

View File

@ -1774,7 +1774,10 @@ func TestStore_IntentionDecision(t *testing.T) {
dst: "ditto",
matchType: structs.IntentionMatchDestination,
defaultDecision: acl.Deny,
expect: structs.IntentionDecisionSummary{Allowed: false},
expect: structs.IntentionDecisionSummary{
Allowed: false,
DefaultAllow: false,
},
},
{
name: "no matching intention and default allow",
@ -1782,7 +1785,10 @@ func TestStore_IntentionDecision(t *testing.T) {
dst: "ditto",
matchType: structs.IntentionMatchDestination,
defaultDecision: acl.Allow,
expect: structs.IntentionDecisionSummary{Allowed: true},
expect: structs.IntentionDecisionSummary{
Allowed: true,
DefaultAllow: true,
},
},
{
name: "denied with permissions",

View File

@ -35,6 +35,22 @@ const (
MeshGatewayModeRemote MeshGatewayMode = "remote"
)
const (
// TODO (freddy) Should we have a TopologySourceMixed when there is a mix of proxy reg and tproxy?
// Currently we label as proxy-registration if ANY instance has the explicit upstream definition.
// TopologySourceRegistration is used to label upstreams or downstreams from explicit upstream definitions
TopologySourceRegistration = "proxy-registration"
// TopologySourceSpecificIntention is used to label upstreams or downstreams from specific intentions
TopologySourceSpecificIntention = "specific-intention"
// TopologySourceWildcardIntention is used to label upstreams or downstreams from wildcard intentions
TopologySourceWildcardIntention = "wildcard-intention"
// TopologySourceDefaultAllow is used to label upstreams or downstreams from default allow ACL policy
TopologySourceDefaultAllow = "default-allow"
)
// MeshGatewayConfig controls how Mesh Gateways are configured and used
// This is a struct to allow for future additions without having more free-hanging
// configuration items all over the place

View File

@ -666,12 +666,14 @@ type IntentionQueryCheckResponse struct {
// - Whether all actions are allowed
// - Whether the matching intention has L7 permissions attached
// - Whether the intention is managed by an external source like k8s
// - Whether there is an exact, on-wildcard, intention referencing the two services
// - Whether there is an exact, or wildcard, intention referencing the two services
// - Whether ACLs are in DefaultAllow mode
type IntentionDecisionSummary struct {
Allowed bool
HasPermissions bool
ExternalSource string
HasExact bool
DefaultAllow bool
}
// IntentionQueryExact holds the parameters for performing a lookup of an

View File

@ -1924,6 +1924,17 @@ type ServiceTopology struct {
// MetricsProtocol is the protocol of the service being queried
MetricsProtocol string
// TransparentProxy describes whether all instances of the proxy
// service are in transparent mode.
TransparentProxy bool
// (Up|Down)streamSources are maps with labels for why each service is being
// returned. Services can be upstreams or downstreams due to
// explicit upstream definition or various types of intention policies:
// specific, wildcard, or default allow.
UpstreamSources map[string]string
DownstreamSources map[string]string
}
// IndexedConfigEntries has its own encoding logic which differs from

View File

@ -19,19 +19,21 @@ import (
// ServiceSummary is used to summarize a service
type ServiceSummary struct {
Kind structs.ServiceKind `json:",omitempty"`
Name string
Datacenter string
Tags []string
Nodes []string
ExternalSources []string
externalSourceSet map[string]struct{} // internal to track uniqueness
checks map[string]*structs.HealthCheck
InstanceCount int
ChecksPassing int
ChecksWarning int
ChecksCritical int
GatewayConfig GatewayConfig
Kind structs.ServiceKind `json:",omitempty"`
Name string
Datacenter string
Tags []string
Nodes []string
ExternalSources []string
externalSourceSet map[string]struct{} // internal to track uniqueness
checks map[string]*structs.HealthCheck
InstanceCount int
ChecksPassing int
ChecksWarning int
ChecksCritical int
GatewayConfig GatewayConfig
TransparentProxy bool
transparentProxySet bool
structs.EnterpriseMeta
}
@ -61,14 +63,16 @@ type ServiceListingSummary struct {
type ServiceTopologySummary struct {
ServiceSummary
Source string
Intention structs.IntentionDecisionSummary
}
type ServiceTopology struct {
Protocol string
Upstreams []*ServiceTopologySummary
Downstreams []*ServiceTopologySummary
FilteredByACLs bool
Protocol string
TransparentProxy bool
Upstreams []*ServiceTopologySummary
Downstreams []*ServiceTopologySummary
FilteredByACLs bool
}
// UINodes is used to list the nodes in a given datacenter. We return a
@ -334,6 +338,7 @@ RPC:
sum := ServiceTopologySummary{
ServiceSummary: *svc,
Intention: out.ServiceTopology.UpstreamDecisions[sn.String()],
Source: out.ServiceTopology.UpstreamSources[sn.String()],
}
upstreamResp = append(upstreamResp, &sum)
}
@ -344,15 +349,17 @@ RPC:
sum := ServiceTopologySummary{
ServiceSummary: *svc,
Intention: out.ServiceTopology.DownstreamDecisions[sn.String()],
Source: out.ServiceTopology.DownstreamSources[sn.String()],
}
downstreamResp = append(downstreamResp, &sum)
}
topo := ServiceTopology{
Protocol: out.ServiceTopology.MetricsProtocol,
Upstreams: upstreamResp,
Downstreams: downstreamResp,
FilteredByACLs: out.FilteredByACLs,
TransparentProxy: out.ServiceTopology.TransparentProxy,
Protocol: out.ServiceTopology.MetricsProtocol,
Upstreams: upstreamResp,
Downstreams: downstreamResp,
FilteredByACLs: out.FilteredByACLs,
}
return topo, nil
}
@ -410,6 +417,17 @@ func summarizeServices(dump structs.ServiceDump, cfg *config.RuntimeConfig, dc s
}
destination.checks[uid] = check
}
// Only consider the target service to be transparent when all its proxy instances are in that mode.
// This is done because the flag is used to display warnings about proxies needing to enable
// transparent proxy mode. If ANY instance isn't in the right mode then the warming applies.
if svc.Proxy.Mode == structs.ProxyModeTransparent && !destination.transparentProxySet {
destination.TransparentProxy = true
}
if svc.Proxy.Mode != structs.ProxyModeTransparent {
destination.TransparentProxy = false
}
destination.transparentProxySet = true
}
for _, tag := range svc.Tags {
found := false

View File

@ -396,6 +396,7 @@ func TestUiServices(t *testing.T) {
// internal accounting that users don't see can be blown away
for _, sum := range summary {
sum.transparentProxySet = false
sum.externalSourceSet = nil
sum.checks = nil
}
@ -1078,12 +1079,7 @@ func TestUIServiceTopology(t *testing.T) {
Address: "198.18.1.2",
Proxy: structs.ConnectProxyConfig{
DestinationServiceName: "api",
Upstreams: structs.Upstreams{
{
DestinationName: "web",
LocalBindPort: 8080,
},
},
Mode: structs.ProxyModeTransparent,
},
},
Checks: structs.HealthChecks{
@ -1210,12 +1206,7 @@ func TestUIServiceTopology(t *testing.T) {
Address: "198.18.1.40",
Proxy: structs.ConnectProxyConfig{
DestinationServiceName: "web",
Upstreams: structs.Upstreams{
{
DestinationName: "redis",
LocalBindPort: 123,
},
},
Mode: structs.ProxyModeTransparent,
},
},
Checks: structs.HealthChecks{
@ -1296,8 +1287,10 @@ func TestUIServiceTopology(t *testing.T) {
}
}
// Add intentions: deny all, ingress -> api, web -> redis with L7 perms, but omit intention for api -> web
// Add ingress config: ingress -> api
// ingress -> api gateway config entry (but no intention)
// wildcard deny intention
// api -> web exact intention
// web -> redis exact intention
{
entries := []structs.ConfigEntryRequest{
{
@ -1318,6 +1311,20 @@ func TestUIServiceTopology(t *testing.T) {
Protocol: "tcp",
},
},
{
Datacenter: "dc1",
Entry: &structs.ServiceIntentionsConfigEntry{
Kind: structs.ServiceIntentions,
Name: "*",
Meta: map[string]string{structs.MetaExternalSource: "nomad"},
Sources: []*structs.SourceIntention{
{
Name: "*",
Action: structs.IntentionActionDeny,
},
},
},
},
{
Datacenter: "dc1",
Entry: &structs.ServiceIntentionsConfigEntry{
@ -1342,12 +1349,11 @@ func TestUIServiceTopology(t *testing.T) {
Datacenter: "dc1",
Entry: &structs.ServiceIntentionsConfigEntry{
Kind: structs.ServiceIntentions,
Name: "*",
Meta: map[string]string{structs.MetaExternalSource: "nomad"},
Name: "web",
Sources: []*structs.SourceIntention{
{
Name: "*",
Action: structs.IntentionActionDeny,
Action: structs.IntentionActionAllow,
Name: "api",
},
},
},
@ -1419,22 +1425,26 @@ func TestUIServiceTopology(t *testing.T) {
require.NoError(r, checkIndex(resp))
expect := ServiceTopology{
Protocol: "tcp",
Protocol: "tcp",
TransparentProxy: false,
Upstreams: []*ServiceTopologySummary{
{
ServiceSummary: ServiceSummary{
Name: "api",
Datacenter: "dc1",
Nodes: []string{"foo"},
InstanceCount: 1,
ChecksPassing: 3,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
Name: "api",
Datacenter: "dc1",
Nodes: []string{"foo"},
InstanceCount: 1,
ChecksPassing: 3,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
TransparentProxy: true,
},
Intention: structs.IntentionDecisionSummary{
DefaultAllow: true,
Allowed: true,
HasPermissions: false,
HasExact: true,
},
Source: structs.TopologySourceRegistration,
},
},
Downstreams: []*ServiceTopologySummary{},
@ -1446,6 +1456,7 @@ func TestUIServiceTopology(t *testing.T) {
for _, u := range result.Upstreams {
u.externalSourceSet = nil
u.checks = nil
u.transparentProxySet = false
}
require.Equal(r, expect, result)
})
@ -1461,45 +1472,49 @@ func TestUIServiceTopology(t *testing.T) {
require.NoError(r, checkIndex(resp))
expect := ServiceTopology{
Protocol: "tcp",
Protocol: "tcp",
TransparentProxy: true,
Downstreams: []*ServiceTopologySummary{
{
ServiceSummary: ServiceSummary{
Name: "ingress",
Kind: structs.ServiceKindIngressGateway,
Datacenter: "dc1",
Nodes: []string{"edge"},
InstanceCount: 1,
ChecksPassing: 1,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
Name: "ingress",
Kind: structs.ServiceKindIngressGateway,
Datacenter: "dc1",
Nodes: []string{"edge"},
InstanceCount: 1,
ChecksPassing: 1,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
TransparentProxy: false,
},
Intention: structs.IntentionDecisionSummary{
DefaultAllow: true,
Allowed: true,
HasPermissions: false,
HasExact: true,
},
Source: structs.TopologySourceRegistration,
},
},
Upstreams: []*ServiceTopologySummary{
{
ServiceSummary: ServiceSummary{
Name: "web",
Datacenter: "dc1",
Nodes: []string{"bar", "baz"},
InstanceCount: 2,
ChecksPassing: 3,
ChecksWarning: 1,
ChecksCritical: 2,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
Name: "web",
Datacenter: "dc1",
Nodes: []string{"bar", "baz"},
InstanceCount: 2,
ChecksPassing: 3,
ChecksWarning: 1,
ChecksCritical: 2,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
TransparentProxy: false,
},
Intention: structs.IntentionDecisionSummary{
Allowed: false,
DefaultAllow: true,
Allowed: true,
HasPermissions: false,
ExternalSource: "nomad",
// From wildcard deny
HasExact: false,
HasExact: true,
},
Source: structs.TopologySourceSpecificIntention,
},
},
FilteredByACLs: false,
@ -1508,10 +1523,12 @@ func TestUIServiceTopology(t *testing.T) {
// Internal accounting that is not returned in JSON response
for _, u := range result.Upstreams {
u.transparentProxySet = false
u.externalSourceSet = nil
u.checks = nil
}
for _, d := range result.Downstreams {
d.transparentProxySet = false
d.externalSourceSet = nil
d.checks = nil
}
@ -1529,43 +1546,47 @@ func TestUIServiceTopology(t *testing.T) {
require.NoError(r, checkIndex(resp))
expect := ServiceTopology{
Protocol: "http",
Protocol: "http",
TransparentProxy: false,
Upstreams: []*ServiceTopologySummary{
{
ServiceSummary: ServiceSummary{
Name: "redis",
Datacenter: "dc1",
Nodes: []string{"zip"},
InstanceCount: 1,
ChecksPassing: 2,
ChecksCritical: 1,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
Name: "redis",
Datacenter: "dc1",
Nodes: []string{"zip"},
InstanceCount: 1,
ChecksPassing: 2,
ChecksCritical: 1,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
TransparentProxy: false,
},
Intention: structs.IntentionDecisionSummary{
DefaultAllow: true,
Allowed: false,
HasPermissions: true,
HasExact: true,
},
Source: structs.TopologySourceRegistration,
},
},
Downstreams: []*ServiceTopologySummary{
{
ServiceSummary: ServiceSummary{
Name: "api",
Datacenter: "dc1",
Nodes: []string{"foo"},
InstanceCount: 1,
ChecksPassing: 3,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
Name: "api",
Datacenter: "dc1",
Nodes: []string{"foo"},
InstanceCount: 1,
ChecksPassing: 3,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
TransparentProxy: true,
},
Intention: structs.IntentionDecisionSummary{
Allowed: false,
DefaultAllow: true,
Allowed: true,
HasPermissions: false,
ExternalSource: "nomad",
// From wildcard deny
HasExact: false,
HasExact: true,
},
Source: structs.TopologySourceSpecificIntention,
},
},
FilteredByACLs: false,
@ -1574,10 +1595,12 @@ func TestUIServiceTopology(t *testing.T) {
// Internal accounting that is not returned in JSON response
for _, u := range result.Upstreams {
u.transparentProxySet = false
u.externalSourceSet = nil
u.checks = nil
}
for _, d := range result.Downstreams {
d.transparentProxySet = false
d.externalSourceSet = nil
d.checks = nil
}
@ -1610,10 +1633,12 @@ func TestUIServiceTopology(t *testing.T) {
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
Intention: structs.IntentionDecisionSummary{
DefaultAllow: true,
Allowed: false,
HasPermissions: true,
HasExact: true,
},
Source: structs.TopologySourceRegistration,
},
},
FilteredByACLs: false,
@ -1622,6 +1647,7 @@ func TestUIServiceTopology(t *testing.T) {
// Internal accounting that is not returned in JSON response
for _, d := range result.Downstreams {
d.transparentProxySet = false
d.externalSourceSet = nil
d.checks = nil
}