mirror of https://github.com/status-im/consul.git
api: expose upstream routing configurations in topology view (#10811)
Some users are defining routing configurations that do not have associated services. This commit surfaces these configs in the topology visualization. Also fixes a minor internal bug with non-transparent proxy upstream/downstream references.
This commit is contained in:
parent
a6d22efb49
commit
45dcc8b553
|
@ -7,14 +7,15 @@ import (
|
||||||
"net/rpc"
|
"net/rpc"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
|
||||||
"github.com/hashicorp/consul/api"
|
|
||||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
|
||||||
"github.com/hashicorp/consul/types"
|
|
||||||
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
|
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
|
||||||
"github.com/hashicorp/raft"
|
"github.com/hashicorp/raft"
|
||||||
"github.com/hashicorp/serf/serf"
|
"github.com/hashicorp/serf/serf"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
|
"github.com/hashicorp/consul/api"
|
||||||
|
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||||
|
"github.com/hashicorp/consul/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func waitForLeader(servers ...*Server) error {
|
func waitForLeader(servers ...*Server) error {
|
||||||
|
@ -976,6 +977,196 @@ func registerTestTopologyEntries(t *testing.T, codec rpc.ClientCodec, token stri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func registerTestRoutingConfigTopologyEntries(t *testing.T, codec rpc.ClientCodec) {
|
||||||
|
registrations := map[string]*structs.RegisterRequest{
|
||||||
|
"Service dashboard": {
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foo",
|
||||||
|
SkipNodeUpdate: true,
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
Kind: structs.ServiceKindTypical,
|
||||||
|
ID: "dashboard",
|
||||||
|
Service: "dashboard",
|
||||||
|
Port: 9002,
|
||||||
|
},
|
||||||
|
Checks: structs.HealthChecks{
|
||||||
|
&structs.HealthCheck{
|
||||||
|
Node: "foo",
|
||||||
|
CheckID: "foo:dashboard",
|
||||||
|
Status: api.HealthPassing,
|
||||||
|
ServiceID: "dashboard",
|
||||||
|
ServiceName: "dashboard",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Service dashboard-proxy": {
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foo",
|
||||||
|
SkipNodeUpdate: true,
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
Kind: structs.ServiceKindConnectProxy,
|
||||||
|
ID: "dashboard-sidecar-proxy",
|
||||||
|
Service: "dashboard-sidecar-proxy",
|
||||||
|
Port: 5000,
|
||||||
|
Address: "198.18.1.0",
|
||||||
|
Proxy: structs.ConnectProxyConfig{
|
||||||
|
DestinationServiceName: "dashboard",
|
||||||
|
DestinationServiceID: "dashboard",
|
||||||
|
LocalServiceAddress: "127.0.0.1",
|
||||||
|
LocalServicePort: 9002,
|
||||||
|
Upstreams: []structs.Upstream{
|
||||||
|
{
|
||||||
|
DestinationType: "service",
|
||||||
|
DestinationName: "routing-config",
|
||||||
|
LocalBindPort: 5000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LocallyRegisteredAsSidecar: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Service counting": {
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foo",
|
||||||
|
SkipNodeUpdate: true,
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
Kind: structs.ServiceKindTypical,
|
||||||
|
ID: "counting",
|
||||||
|
Service: "counting",
|
||||||
|
Port: 9003,
|
||||||
|
Address: "198.18.1.1",
|
||||||
|
},
|
||||||
|
Checks: structs.HealthChecks{
|
||||||
|
&structs.HealthCheck{
|
||||||
|
Node: "foo",
|
||||||
|
CheckID: "foo:api",
|
||||||
|
Status: api.HealthPassing,
|
||||||
|
ServiceID: "counting",
|
||||||
|
ServiceName: "counting",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Service counting-proxy": {
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foo",
|
||||||
|
SkipNodeUpdate: true,
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
Kind: structs.ServiceKindConnectProxy,
|
||||||
|
ID: "counting-proxy",
|
||||||
|
Service: "counting-proxy",
|
||||||
|
Port: 5001,
|
||||||
|
Address: "198.18.1.1",
|
||||||
|
Proxy: structs.ConnectProxyConfig{
|
||||||
|
DestinationServiceName: "counting",
|
||||||
|
},
|
||||||
|
LocallyRegisteredAsSidecar: true,
|
||||||
|
},
|
||||||
|
Checks: structs.HealthChecks{
|
||||||
|
&structs.HealthCheck{
|
||||||
|
Node: "foo",
|
||||||
|
CheckID: "foo:counting-proxy",
|
||||||
|
Status: api.HealthPassing,
|
||||||
|
ServiceID: "counting-proxy",
|
||||||
|
ServiceName: "counting-proxy",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Service counting-v2": {
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foo",
|
||||||
|
SkipNodeUpdate: true,
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
Kind: structs.ServiceKindTypical,
|
||||||
|
ID: "counting-v2",
|
||||||
|
Service: "counting-v2",
|
||||||
|
Port: 9004,
|
||||||
|
Address: "198.18.1.2",
|
||||||
|
},
|
||||||
|
Checks: structs.HealthChecks{
|
||||||
|
&structs.HealthCheck{
|
||||||
|
Node: "foo",
|
||||||
|
CheckID: "foo:api",
|
||||||
|
Status: api.HealthPassing,
|
||||||
|
ServiceID: "counting-v2",
|
||||||
|
ServiceName: "counting-v2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Service counting-v2-proxy": {
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foo",
|
||||||
|
SkipNodeUpdate: true,
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
Kind: structs.ServiceKindConnectProxy,
|
||||||
|
ID: "counting-v2-proxy",
|
||||||
|
Service: "counting-v2-proxy",
|
||||||
|
Port: 5002,
|
||||||
|
Address: "198.18.1.2",
|
||||||
|
Proxy: structs.ConnectProxyConfig{
|
||||||
|
DestinationServiceName: "counting-v2",
|
||||||
|
},
|
||||||
|
LocallyRegisteredAsSidecar: true,
|
||||||
|
},
|
||||||
|
Checks: structs.HealthChecks{
|
||||||
|
&structs.HealthCheck{
|
||||||
|
Node: "foo",
|
||||||
|
CheckID: "foo:counting-v2-proxy",
|
||||||
|
Status: api.HealthPassing,
|
||||||
|
ServiceID: "counting-v2-proxy",
|
||||||
|
ServiceName: "counting-v2-proxy",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
registerTestCatalogEntriesMap(t, codec, registrations)
|
||||||
|
|
||||||
|
entries := []structs.ConfigEntryRequest{
|
||||||
|
{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Entry: &structs.ProxyConfigEntry{
|
||||||
|
Kind: structs.ProxyDefaults,
|
||||||
|
Name: structs.ProxyConfigGlobal,
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"protocol": "http",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Entry: &structs.ServiceRouterConfigEntry{
|
||||||
|
Kind: structs.ServiceRouter,
|
||||||
|
Name: "routing-config",
|
||||||
|
Routes: []structs.ServiceRoute{
|
||||||
|
{
|
||||||
|
Match: &structs.ServiceRouteMatch{
|
||||||
|
HTTP: &structs.ServiceRouteHTTPMatch{
|
||||||
|
PathPrefix: "/v2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Destination: &structs.ServiceRouteDestination{
|
||||||
|
Service: "counting-v2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Match: &structs.ServiceRouteMatch{
|
||||||
|
HTTP: &structs.ServiceRouteHTTPMatch{
|
||||||
|
PathPrefix: "/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Destination: &structs.ServiceRouteDestination{
|
||||||
|
Service: "counting",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, req := range entries {
|
||||||
|
var out bool
|
||||||
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &req, &out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func registerIntentionUpstreamEntries(t *testing.T, codec rpc.ClientCodec, token string) {
|
func registerIntentionUpstreamEntries(t *testing.T, codec rpc.ClientCodec, token string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|
|
@ -1902,6 +1902,58 @@ func TestInternal_ServiceTopology(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInternal_ServiceTopology_RoutingConfig(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("too slow for testing.Short")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Parallel()
|
||||||
|
dir1, s1 := testServer(t)
|
||||||
|
defer os.RemoveAll(dir1)
|
||||||
|
defer s1.Shutdown()
|
||||||
|
|
||||||
|
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||||
|
|
||||||
|
codec := rpcClient(t, s1)
|
||||||
|
defer codec.Close()
|
||||||
|
|
||||||
|
// dashboard -> routing-config -> { counting, counting-v2 }
|
||||||
|
registerTestRoutingConfigTopologyEntries(t, codec)
|
||||||
|
|
||||||
|
t.Run("dashboard", func(t *testing.T) {
|
||||||
|
retry.Run(t, func(r *retry.R) {
|
||||||
|
args := structs.ServiceSpecificRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
ServiceName: "dashboard",
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
|
||||||
|
require.Empty(r, out.ServiceTopology.Downstreams)
|
||||||
|
require.Empty(r, out.ServiceTopology.DownstreamDecisions)
|
||||||
|
require.Empty(r, out.ServiceTopology.DownstreamSources)
|
||||||
|
|
||||||
|
// routing-config will not appear as an Upstream service
|
||||||
|
// but will be present in UpstreamSources as a k-v pair.
|
||||||
|
require.Empty(r, out.ServiceTopology.Upstreams)
|
||||||
|
|
||||||
|
expectUp := map[string]structs.IntentionDecisionSummary{
|
||||||
|
"routing-config": {DefaultAllow: true, Allowed: true},
|
||||||
|
}
|
||||||
|
require.Equal(r, expectUp, out.ServiceTopology.UpstreamDecisions)
|
||||||
|
|
||||||
|
expectUpstreamSources := map[string]string{
|
||||||
|
"routing-config": structs.TopologySourceRoutingConfig,
|
||||||
|
}
|
||||||
|
require.Equal(r, expectUpstreamSources, out.ServiceTopology.UpstreamSources)
|
||||||
|
|
||||||
|
require.False(r, out.ServiceTopology.TransparentProxy)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestInternal_ServiceTopology_ACL(t *testing.T) {
|
func TestInternal_ServiceTopology_ACL(t *testing.T) {
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("too slow for testing.Short")
|
t.Skip("too slow for testing.Short")
|
||||||
|
|
|
@ -3092,44 +3092,67 @@ func (s *Store) ServiceTopology(
|
||||||
maxIdx = idx
|
maxIdx = idx
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var upstreamSources = make(map[string]string)
|
||||||
seenUpstreams = make(map[string]struct{})
|
|
||||||
upstreamSources = make(map[string]string)
|
|
||||||
)
|
|
||||||
for _, un := range upstreamNames {
|
for _, un := range upstreamNames {
|
||||||
if _, ok := seenUpstreams[un.String()]; !ok {
|
|
||||||
seenUpstreams[un.String()] = struct{}{}
|
|
||||||
}
|
|
||||||
upstreamSources[un.String()] = structs.TopologySourceRegistration
|
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)
|
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
|
// Only transparent proxies have upstreams from intentions
|
||||||
switch {
|
if hasTransparent {
|
||||||
case svc.Decision.HasExact:
|
idx, intentionUpstreams, err := s.intentionTopologyTxn(tx, ws, sn, false, defaultAllow)
|
||||||
source = structs.TopologySourceSpecificIntention
|
if err != nil {
|
||||||
case svc.Decision.DefaultAllow:
|
return 0, nil, err
|
||||||
source = structs.TopologySourceDefaultAllow
|
|
||||||
default:
|
|
||||||
source = structs.TopologySourceWildcardIntention
|
|
||||||
}
|
}
|
||||||
upstreamSources[svc.Name.String()] = source
|
if idx > maxIdx {
|
||||||
|
maxIdx = idx
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, svc := range intentionUpstreams {
|
||||||
|
if _, ok := upstreamSources[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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
matchEntry := structs.IntentionMatchEntry{
|
||||||
|
Namespace: entMeta.NamespaceOrDefault(),
|
||||||
|
Name: service,
|
||||||
|
}
|
||||||
|
_, 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, un := range upstreamNames {
|
||||||
|
decision, err := s.IntentionDecision(un.Name, un.NamespaceOrDefault(), srcIntentions, structs.IntentionMatchDestination, defaultAllow, false)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, fmt.Errorf("failed to get intention decision from (%s) to (%s): %v",
|
||||||
|
sn.String(), un.String(), err)
|
||||||
|
}
|
||||||
|
upstreamDecisions[un.String()] = decision
|
||||||
}
|
}
|
||||||
|
|
||||||
idx, unfilteredUpstreams, err := s.combinedServiceNodesTxn(tx, ws, upstreamNames)
|
idx, unfilteredUpstreams, err := s.combinedServiceNodesTxn(tx, ws, upstreamNames)
|
||||||
|
@ -3154,28 +3177,29 @@ func (s *Store) ServiceTopology(
|
||||||
upstreams = append(upstreams, upstream)
|
upstreams = append(upstreams, upstream)
|
||||||
}
|
}
|
||||||
|
|
||||||
matchEntry := structs.IntentionMatchEntry{
|
var foundUpstreams = make(map[structs.ServiceName]struct{})
|
||||||
Namespace: entMeta.NamespaceOrDefault(),
|
for _, csn := range upstreams {
|
||||||
Name: service,
|
foundUpstreams[csn.Service.CompoundServiceName()] = struct{}{}
|
||||||
}
|
}
|
||||||
_, srcIntentions, err := compatIntentionMatchOneTxn(
|
|
||||||
tx,
|
|
||||||
ws,
|
|
||||||
matchEntry,
|
|
||||||
|
|
||||||
// The given service is a source relative to its upstreams
|
// Check upstream names that had no service instances to see if they are routing config.
|
||||||
structs.IntentionMatchSource,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, fmt.Errorf("failed to query intentions for %s", sn.String())
|
|
||||||
}
|
|
||||||
for _, un := range upstreamNames {
|
for _, un := range upstreamNames {
|
||||||
decision, err := s.IntentionDecision(un.Name, un.NamespaceOrDefault(), srcIntentions, structs.IntentionMatchDestination, defaultAllow, false)
|
if _, ok := foundUpstreams[un]; ok {
|
||||||
if err != nil {
|
continue
|
||||||
return 0, nil, fmt.Errorf("failed to get intention decision from (%s) to (%s): %v",
|
}
|
||||||
sn.String(), un.String(), err)
|
|
||||||
|
for _, kind := range serviceGraphKinds {
|
||||||
|
idx, entry, err := configEntryTxn(tx, ws, kind, un.Name, &un.EnterpriseMeta)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
if entry != nil {
|
||||||
|
upstreamSources[un.String()] = structs.TopologySourceRoutingConfig
|
||||||
|
}
|
||||||
|
if idx > maxIdx {
|
||||||
|
maxIdx = idx
|
||||||
|
}
|
||||||
}
|
}
|
||||||
upstreamDecisions[un.String()] = decision
|
|
||||||
}
|
}
|
||||||
|
|
||||||
idx, downstreamNames, err := s.downstreamsForServiceTxn(tx, ws, dc, sn)
|
idx, downstreamNames, err := s.downstreamsForServiceTxn(tx, ws, dc, sn)
|
||||||
|
@ -3186,14 +3210,8 @@ func (s *Store) ServiceTopology(
|
||||||
maxIdx = idx
|
maxIdx = idx
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var downstreamSources = make(map[string]string)
|
||||||
seenDownstreams = make(map[string]struct{})
|
|
||||||
downstreamSources = make(map[string]string)
|
|
||||||
)
|
|
||||||
for _, dn := range downstreamNames {
|
for _, dn := range downstreamNames {
|
||||||
if _, ok := seenDownstreams[dn.String()]; !ok {
|
|
||||||
seenDownstreams[dn.String()] = struct{}{}
|
|
||||||
}
|
|
||||||
downstreamSources[dn.String()] = structs.TopologySourceRegistration
|
downstreamSources[dn.String()] = structs.TopologySourceRegistration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3207,7 +3225,7 @@ func (s *Store) ServiceTopology(
|
||||||
|
|
||||||
downstreamDecisions := make(map[string]structs.IntentionDecisionSummary)
|
downstreamDecisions := make(map[string]structs.IntentionDecisionSummary)
|
||||||
for _, svc := range intentionDownstreams {
|
for _, svc := range intentionDownstreams {
|
||||||
if _, ok := seenDownstreams[svc.Name.String()]; ok {
|
if _, ok := downstreamSources[svc.Name.String()]; ok {
|
||||||
// Avoid duplicating entry
|
// Avoid duplicating entry
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -3226,6 +3244,26 @@ func (s *Store) ServiceTopology(
|
||||||
downstreamSources[svc.Name.String()] = source
|
downstreamSources[svc.Name.String()] = source
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, 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())
|
||||||
|
}
|
||||||
|
for _, dn := range downstreamNames {
|
||||||
|
decision, err := s.IntentionDecision(dn.Name, dn.NamespaceOrDefault(), dstIntentions, structs.IntentionMatchSource, defaultAllow, false)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, fmt.Errorf("failed to get intention decision from (%s) to (%s): %v",
|
||||||
|
dn.String(), sn.String(), err)
|
||||||
|
}
|
||||||
|
downstreamDecisions[dn.String()] = decision
|
||||||
|
}
|
||||||
|
|
||||||
idx, unfilteredDownstreams, err := s.combinedServiceNodesTxn(tx, ws, downstreamNames)
|
idx, unfilteredDownstreams, err := s.combinedServiceNodesTxn(tx, ws, downstreamNames)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, fmt.Errorf("failed to get downstreams for %q: %v", sn.String(), err)
|
return 0, nil, fmt.Errorf("failed to get downstreams for %q: %v", sn.String(), err)
|
||||||
|
@ -3251,31 +3289,14 @@ func (s *Store) ServiceTopology(
|
||||||
sn = structs.NewServiceName(downstream.Service.Proxy.DestinationServiceName, &downstream.Service.EnterpriseMeta)
|
sn = structs.NewServiceName(downstream.Service.Proxy.DestinationServiceName, &downstream.Service.EnterpriseMeta)
|
||||||
}
|
}
|
||||||
if _, ok := tproxyMap[sn]; !ok && downstreamSources[sn.String()] != structs.TopologySourceRegistration {
|
if _, ok := tproxyMap[sn]; !ok && downstreamSources[sn.String()] != structs.TopologySourceRegistration {
|
||||||
|
// If downstream is not a transparent proxy, remove references
|
||||||
|
delete(downstreamSources, sn.String())
|
||||||
|
delete(downstreamDecisions, sn.String())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
downstreams = append(downstreams, downstream)
|
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())
|
|
||||||
}
|
|
||||||
for _, dn := range downstreamNames {
|
|
||||||
decision, err := s.IntentionDecision(dn.Name, dn.NamespaceOrDefault(), dstIntentions, structs.IntentionMatchSource, defaultAllow, false)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, fmt.Errorf("failed to get intention decision from (%s) to (%s): %v",
|
|
||||||
dn.String(), sn.String(), err)
|
|
||||||
}
|
|
||||||
downstreamDecisions[dn.String()] = decision
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := &structs.ServiceTopology{
|
resp := &structs.ServiceTopology{
|
||||||
TransparentProxy: fullyTransparent,
|
TransparentProxy: fullyTransparent,
|
||||||
MetricsProtocol: protocol,
|
MetricsProtocol: protocol,
|
||||||
|
|
|
@ -39,17 +39,21 @@ const (
|
||||||
const (
|
const (
|
||||||
// TODO (freddy) Should we have a TopologySourceMixed when there is a mix of proxy reg and tproxy?
|
// 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.
|
// 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 is used to label upstreams or downstreams from explicit upstream definitions.
|
||||||
TopologySourceRegistration = "proxy-registration"
|
TopologySourceRegistration = "proxy-registration"
|
||||||
|
|
||||||
// TopologySourceSpecificIntention is used to label upstreams or downstreams from specific intentions
|
// TopologySourceSpecificIntention is used to label upstreams or downstreams from specific intentions.
|
||||||
TopologySourceSpecificIntention = "specific-intention"
|
TopologySourceSpecificIntention = "specific-intention"
|
||||||
|
|
||||||
// TopologySourceWildcardIntention is used to label upstreams or downstreams from wildcard intentions
|
// TopologySourceWildcardIntention is used to label upstreams or downstreams from wildcard intentions.
|
||||||
TopologySourceWildcardIntention = "wildcard-intention"
|
TopologySourceWildcardIntention = "wildcard-intention"
|
||||||
|
|
||||||
// TopologySourceDefaultAllow is used to label upstreams or downstreams from default allow ACL policy
|
// TopologySourceDefaultAllow is used to label upstreams or downstreams from default allow ACL policy.
|
||||||
TopologySourceDefaultAllow = "default-allow"
|
TopologySourceDefaultAllow = "default-allow"
|
||||||
|
|
||||||
|
// TopologySourceRoutingConfig is used to label upstreams that are not backed by a service instance
|
||||||
|
// and are simply used for routing configurations.
|
||||||
|
TopologySourceRoutingConfig = "routing-config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MeshGatewayConfig controls how Mesh Gateways are configured and used
|
// MeshGatewayConfig controls how Mesh Gateways are configured and used
|
||||||
|
|
|
@ -343,6 +343,21 @@ RPC:
|
||||||
}
|
}
|
||||||
upstreamResp = append(upstreamResp, &sum)
|
upstreamResp = append(upstreamResp, &sum)
|
||||||
}
|
}
|
||||||
|
for k, v := range out.ServiceTopology.UpstreamSources {
|
||||||
|
if v == structs.TopologySourceRoutingConfig {
|
||||||
|
sn := structs.ServiceNameFromString(k)
|
||||||
|
sum := ServiceTopologySummary{
|
||||||
|
ServiceSummary: ServiceSummary{
|
||||||
|
Datacenter: args.Datacenter,
|
||||||
|
Name: sn.Name,
|
||||||
|
EnterpriseMeta: sn.EnterpriseMeta,
|
||||||
|
},
|
||||||
|
Intention: out.ServiceTopology.UpstreamDecisions[sn.String()],
|
||||||
|
Source: out.ServiceTopology.UpstreamSources[sn.String()],
|
||||||
|
}
|
||||||
|
upstreamResp = append(upstreamResp, &sum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sortedDownstreams := prepSummaryOutput(downstreams, true)
|
sortedDownstreams := prepSummaryOutput(downstreams, true)
|
||||||
for _, svc := range sortedDownstreams {
|
for _, svc := range sortedDownstreams {
|
||||||
|
|
|
@ -12,15 +12,16 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/config"
|
"github.com/hashicorp/consul/agent/config"
|
||||||
"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/sdk/testutil"
|
"github.com/hashicorp/consul/sdk/testutil"
|
||||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||||
"github.com/hashicorp/consul/testrpc"
|
"github.com/hashicorp/consul/testrpc"
|
||||||
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUiIndex(t *testing.T) {
|
func TestUiIndex(t *testing.T) {
|
||||||
|
@ -1397,34 +1398,59 @@ func TestUIServiceTopology(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("request without kind", func(t *testing.T) {
|
type testCase struct {
|
||||||
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/ingress", nil)
|
name string
|
||||||
resp := httptest.NewRecorder()
|
httpReq *http.Request
|
||||||
obj, err := a.srv.UIServiceTopology(resp, req)
|
want *ServiceTopology
|
||||||
require.Nil(t, err)
|
wantErr string
|
||||||
require.Nil(t, obj)
|
}
|
||||||
require.Equal(t, "Missing service kind", resp.Body.String())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("request with unsupported kind", func(t *testing.T) {
|
run := func(t *testing.T, tc testCase) {
|
||||||
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/ingress?kind=not-a-kind", nil)
|
|
||||||
resp := httptest.NewRecorder()
|
|
||||||
obj, err := a.srv.UIServiceTopology(resp, req)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Nil(t, obj)
|
|
||||||
require.Equal(t, `Unsupported service kind "not-a-kind"`, resp.Body.String())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("ingress", func(t *testing.T) {
|
|
||||||
retry.Run(t, func(r *retry.R) {
|
retry.Run(t, func(r *retry.R) {
|
||||||
// Request topology for ingress
|
|
||||||
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/ingress?kind=ingress-gateway", nil)
|
|
||||||
resp := httptest.NewRecorder()
|
resp := httptest.NewRecorder()
|
||||||
obj, err := a.srv.UIServiceTopology(resp, req)
|
obj, err := a.srv.UIServiceTopology(resp, tc.httpReq)
|
||||||
assert.Nil(r, err)
|
assert.Nil(r, err)
|
||||||
require.NoError(r, checkIndex(resp))
|
|
||||||
|
|
||||||
expect := ServiceTopology{
|
if tc.wantErr != "" {
|
||||||
|
assert.Nil(r, tc.want) // should not define a non-nil want
|
||||||
|
require.Equal(r, tc.wantErr, resp.Body.String())
|
||||||
|
require.Nil(r, obj)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(r, checkIndex(resp))
|
||||||
|
require.NotNil(r, obj)
|
||||||
|
result := obj.(ServiceTopology)
|
||||||
|
clearUnexportedFields(result)
|
||||||
|
|
||||||
|
require.Equal(r, *tc.want, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
tcs := []testCase{
|
||||||
|
{
|
||||||
|
name: "request without kind",
|
||||||
|
httpReq: func() *http.Request {
|
||||||
|
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/ingress", nil)
|
||||||
|
return req
|
||||||
|
}(),
|
||||||
|
wantErr: "Missing service kind",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "request with unsupported kind",
|
||||||
|
httpReq: func() *http.Request {
|
||||||
|
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/ingress?kind=not-a-kind", nil)
|
||||||
|
return req
|
||||||
|
}(),
|
||||||
|
wantErr: `Unsupported service kind "not-a-kind"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ingress",
|
||||||
|
httpReq: func() *http.Request {
|
||||||
|
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/ingress?kind=ingress-gateway", nil)
|
||||||
|
return req
|
||||||
|
}(),
|
||||||
|
want: &ServiceTopology{
|
||||||
Protocol: "tcp",
|
Protocol: "tcp",
|
||||||
TransparentProxy: false,
|
TransparentProxy: false,
|
||||||
Upstreams: []*ServiceTopologySummary{
|
Upstreams: []*ServiceTopologySummary{
|
||||||
|
@ -1449,29 +1475,15 @@ func TestUIServiceTopology(t *testing.T) {
|
||||||
},
|
},
|
||||||
Downstreams: []*ServiceTopologySummary{},
|
Downstreams: []*ServiceTopologySummary{},
|
||||||
FilteredByACLs: false,
|
FilteredByACLs: false,
|
||||||
}
|
},
|
||||||
result := obj.(ServiceTopology)
|
},
|
||||||
|
{
|
||||||
// Internal accounting that is not returned in JSON response
|
name: "api",
|
||||||
for _, u := range result.Upstreams {
|
httpReq: func() *http.Request {
|
||||||
u.externalSourceSet = nil
|
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/api?kind=", nil)
|
||||||
u.checks = nil
|
return req
|
||||||
u.transparentProxySet = false
|
}(),
|
||||||
}
|
want: &ServiceTopology{
|
||||||
require.Equal(r, expect, result)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("api", func(t *testing.T) {
|
|
||||||
retry.Run(t, func(r *retry.R) {
|
|
||||||
// Request topology for api
|
|
||||||
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/api?kind=", nil)
|
|
||||||
resp := httptest.NewRecorder()
|
|
||||||
obj, err := a.srv.UIServiceTopology(resp, req)
|
|
||||||
assert.Nil(r, err)
|
|
||||||
require.NoError(r, checkIndex(resp))
|
|
||||||
|
|
||||||
expect := ServiceTopology{
|
|
||||||
Protocol: "tcp",
|
Protocol: "tcp",
|
||||||
TransparentProxy: true,
|
TransparentProxy: true,
|
||||||
Downstreams: []*ServiceTopologySummary{
|
Downstreams: []*ServiceTopologySummary{
|
||||||
|
@ -1518,34 +1530,15 @@ func TestUIServiceTopology(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
FilteredByACLs: false,
|
FilteredByACLs: false,
|
||||||
}
|
},
|
||||||
result := obj.(ServiceTopology)
|
},
|
||||||
|
{
|
||||||
// Internal accounting that is not returned in JSON response
|
name: "web",
|
||||||
for _, u := range result.Upstreams {
|
httpReq: func() *http.Request {
|
||||||
u.transparentProxySet = false
|
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/web?kind=", nil)
|
||||||
u.externalSourceSet = nil
|
return req
|
||||||
u.checks = nil
|
}(),
|
||||||
}
|
want: &ServiceTopology{
|
||||||
for _, d := range result.Downstreams {
|
|
||||||
d.transparentProxySet = false
|
|
||||||
d.externalSourceSet = nil
|
|
||||||
d.checks = nil
|
|
||||||
}
|
|
||||||
require.Equal(r, expect, result)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("web", func(t *testing.T) {
|
|
||||||
retry.Run(t, func(r *retry.R) {
|
|
||||||
// Request topology for web
|
|
||||||
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/web?kind=", nil)
|
|
||||||
resp := httptest.NewRecorder()
|
|
||||||
obj, err := a.srv.UIServiceTopology(resp, req)
|
|
||||||
assert.Nil(r, err)
|
|
||||||
require.NoError(r, checkIndex(resp))
|
|
||||||
|
|
||||||
expect := ServiceTopology{
|
|
||||||
Protocol: "http",
|
Protocol: "http",
|
||||||
TransparentProxy: false,
|
TransparentProxy: false,
|
||||||
Upstreams: []*ServiceTopologySummary{
|
Upstreams: []*ServiceTopologySummary{
|
||||||
|
@ -1590,36 +1583,18 @@ func TestUIServiceTopology(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
FilteredByACLs: false,
|
FilteredByACLs: false,
|
||||||
}
|
},
|
||||||
result := obj.(ServiceTopology)
|
},
|
||||||
|
{
|
||||||
// Internal accounting that is not returned in JSON response
|
name: "redis",
|
||||||
for _, u := range result.Upstreams {
|
httpReq: func() *http.Request {
|
||||||
u.transparentProxySet = false
|
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/redis?kind=", nil)
|
||||||
u.externalSourceSet = nil
|
return req
|
||||||
u.checks = nil
|
}(),
|
||||||
}
|
want: &ServiceTopology{
|
||||||
for _, d := range result.Downstreams {
|
Protocol: "http",
|
||||||
d.transparentProxySet = false
|
TransparentProxy: false,
|
||||||
d.externalSourceSet = nil
|
Upstreams: []*ServiceTopologySummary{},
|
||||||
d.checks = nil
|
|
||||||
}
|
|
||||||
require.Equal(r, expect, result)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("redis", func(t *testing.T) {
|
|
||||||
retry.Run(t, func(r *retry.R) {
|
|
||||||
// Request topology for redis
|
|
||||||
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/redis?kind=", nil)
|
|
||||||
resp := httptest.NewRecorder()
|
|
||||||
obj, err := a.srv.UIServiceTopology(resp, req)
|
|
||||||
assert.Nil(r, err)
|
|
||||||
require.NoError(r, checkIndex(resp))
|
|
||||||
|
|
||||||
expect := ServiceTopology{
|
|
||||||
Protocol: "http",
|
|
||||||
Upstreams: []*ServiceTopologySummary{},
|
|
||||||
Downstreams: []*ServiceTopologySummary{
|
Downstreams: []*ServiceTopologySummary{
|
||||||
{
|
{
|
||||||
ServiceSummary: ServiceSummary{
|
ServiceSummary: ServiceSummary{
|
||||||
|
@ -1642,18 +1617,327 @@ func TestUIServiceTopology(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
FilteredByACLs: false,
|
FilteredByACLs: false,
|
||||||
}
|
},
|
||||||
result := obj.(ServiceTopology)
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// Internal accounting that is not returned in JSON response
|
for _, tc := range tcs {
|
||||||
for _, d := range result.Downstreams {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
d.transparentProxySet = false
|
run(t, tc)
|
||||||
d.externalSourceSet = nil
|
|
||||||
d.checks = nil
|
|
||||||
}
|
|
||||||
require.Equal(r, expect, result)
|
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearUnexportedFields sets unexported members of ServiceTopology to their
|
||||||
|
// type defaults, since the fields are not marshalled in the JSON response.
|
||||||
|
func clearUnexportedFields(result ServiceTopology) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIServiceTopology_RoutingConfigs(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("too slow for testing.Short")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
a := NewTestAgent(t, "")
|
||||||
|
defer a.Shutdown()
|
||||||
|
|
||||||
|
// Register dashboard -> routing-config -> { counting, counting-v2 }
|
||||||
|
{
|
||||||
|
registrations := map[string]*structs.RegisterRequest{
|
||||||
|
"Service dashboard": {
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foo",
|
||||||
|
SkipNodeUpdate: true,
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
Kind: structs.ServiceKindTypical,
|
||||||
|
ID: "dashboard",
|
||||||
|
Service: "dashboard",
|
||||||
|
Port: 9002,
|
||||||
|
},
|
||||||
|
Checks: structs.HealthChecks{
|
||||||
|
&structs.HealthCheck{
|
||||||
|
Node: "foo",
|
||||||
|
CheckID: "foo:dashboard",
|
||||||
|
Status: api.HealthPassing,
|
||||||
|
ServiceID: "dashboard",
|
||||||
|
ServiceName: "dashboard",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Service dashboard-proxy": {
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foo",
|
||||||
|
SkipNodeUpdate: true,
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
Kind: structs.ServiceKindConnectProxy,
|
||||||
|
ID: "dashboard-sidecar-proxy",
|
||||||
|
Service: "dashboard-sidecar-proxy",
|
||||||
|
Port: 5000,
|
||||||
|
Address: "198.18.1.0",
|
||||||
|
Proxy: structs.ConnectProxyConfig{
|
||||||
|
DestinationServiceName: "dashboard",
|
||||||
|
DestinationServiceID: "dashboard",
|
||||||
|
LocalServiceAddress: "127.0.0.1",
|
||||||
|
LocalServicePort: 9002,
|
||||||
|
Upstreams: []structs.Upstream{
|
||||||
|
{
|
||||||
|
DestinationType: "service",
|
||||||
|
DestinationName: "routing-config",
|
||||||
|
LocalBindPort: 5000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LocallyRegisteredAsSidecar: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Service counting": {
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foo",
|
||||||
|
SkipNodeUpdate: true,
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
Kind: structs.ServiceKindTypical,
|
||||||
|
ID: "counting",
|
||||||
|
Service: "counting",
|
||||||
|
Port: 9003,
|
||||||
|
Address: "198.18.1.1",
|
||||||
|
},
|
||||||
|
Checks: structs.HealthChecks{
|
||||||
|
&structs.HealthCheck{
|
||||||
|
Node: "foo",
|
||||||
|
CheckID: "foo:api",
|
||||||
|
Status: api.HealthPassing,
|
||||||
|
ServiceID: "counting",
|
||||||
|
ServiceName: "counting",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Service counting-proxy": {
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foo",
|
||||||
|
SkipNodeUpdate: true,
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
Kind: structs.ServiceKindConnectProxy,
|
||||||
|
ID: "counting-proxy",
|
||||||
|
Service: "counting-proxy",
|
||||||
|
Port: 5001,
|
||||||
|
Address: "198.18.1.1",
|
||||||
|
Proxy: structs.ConnectProxyConfig{
|
||||||
|
DestinationServiceName: "counting",
|
||||||
|
},
|
||||||
|
LocallyRegisteredAsSidecar: true,
|
||||||
|
},
|
||||||
|
Checks: structs.HealthChecks{
|
||||||
|
&structs.HealthCheck{
|
||||||
|
Node: "foo",
|
||||||
|
CheckID: "foo:counting-proxy",
|
||||||
|
Status: api.HealthPassing,
|
||||||
|
ServiceID: "counting-proxy",
|
||||||
|
ServiceName: "counting-proxy",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Service counting-v2": {
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foo",
|
||||||
|
SkipNodeUpdate: true,
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
Kind: structs.ServiceKindTypical,
|
||||||
|
ID: "counting-v2",
|
||||||
|
Service: "counting-v2",
|
||||||
|
Port: 9004,
|
||||||
|
Address: "198.18.1.2",
|
||||||
|
},
|
||||||
|
Checks: structs.HealthChecks{
|
||||||
|
&structs.HealthCheck{
|
||||||
|
Node: "foo",
|
||||||
|
CheckID: "foo:api",
|
||||||
|
Status: api.HealthPassing,
|
||||||
|
ServiceID: "counting-v2",
|
||||||
|
ServiceName: "counting-v2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Service counting-v2-proxy": {
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "foo",
|
||||||
|
SkipNodeUpdate: true,
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
Kind: structs.ServiceKindConnectProxy,
|
||||||
|
ID: "counting-v2-proxy",
|
||||||
|
Service: "counting-v2-proxy",
|
||||||
|
Port: 5002,
|
||||||
|
Address: "198.18.1.2",
|
||||||
|
Proxy: structs.ConnectProxyConfig{
|
||||||
|
DestinationServiceName: "counting-v2",
|
||||||
|
},
|
||||||
|
LocallyRegisteredAsSidecar: true,
|
||||||
|
},
|
||||||
|
Checks: structs.HealthChecks{
|
||||||
|
&structs.HealthCheck{
|
||||||
|
Node: "foo",
|
||||||
|
CheckID: "foo:counting-v2-proxy",
|
||||||
|
Status: api.HealthPassing,
|
||||||
|
ServiceID: "counting-v2-proxy",
|
||||||
|
ServiceName: "counting-v2-proxy",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, args := range registrations {
|
||||||
|
var out struct{}
|
||||||
|
require.NoError(t, a.RPC("Catalog.Register", args, &out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
entries := []structs.ConfigEntryRequest{
|
||||||
|
{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Entry: &structs.ProxyConfigEntry{
|
||||||
|
Kind: structs.ProxyDefaults,
|
||||||
|
Name: structs.ProxyConfigGlobal,
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"protocol": "http",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Entry: &structs.ServiceRouterConfigEntry{
|
||||||
|
Kind: structs.ServiceRouter,
|
||||||
|
Name: "routing-config",
|
||||||
|
Routes: []structs.ServiceRoute{
|
||||||
|
{
|
||||||
|
Match: &structs.ServiceRouteMatch{
|
||||||
|
HTTP: &structs.ServiceRouteHTTPMatch{
|
||||||
|
PathPrefix: "/v2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Destination: &structs.ServiceRouteDestination{
|
||||||
|
Service: "counting-v2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Match: &structs.ServiceRouteMatch{
|
||||||
|
HTTP: &structs.ServiceRouteHTTPMatch{
|
||||||
|
PathPrefix: "/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Destination: &structs.ServiceRouteDestination{
|
||||||
|
Service: "counting",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, req := range entries {
|
||||||
|
out := false
|
||||||
|
require.NoError(t, a.RPC("ConfigEntry.Apply", &req, &out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
httpReq *http.Request
|
||||||
|
want *ServiceTopology
|
||||||
|
wantErr string
|
||||||
|
}
|
||||||
|
|
||||||
|
run := func(t *testing.T, tc testCase) {
|
||||||
|
retry.Run(t, func(r *retry.R) {
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
obj, err := a.srv.UIServiceTopology(resp, tc.httpReq)
|
||||||
|
assert.Nil(r, err)
|
||||||
|
|
||||||
|
if tc.wantErr != "" {
|
||||||
|
assert.Nil(r, tc.want) // should not define a non-nil want
|
||||||
|
require.Equal(r, tc.wantErr, resp.Body.String())
|
||||||
|
require.Nil(r, obj)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(r, checkIndex(resp))
|
||||||
|
require.NotNil(r, obj)
|
||||||
|
result := obj.(ServiceTopology)
|
||||||
|
clearUnexportedFields(result)
|
||||||
|
|
||||||
|
require.Equal(r, *tc.want, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
tcs := []testCase{
|
||||||
|
{
|
||||||
|
name: "dashboard has upstream routing-config",
|
||||||
|
httpReq: func() *http.Request {
|
||||||
|
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/dashboard?kind=", nil)
|
||||||
|
return req
|
||||||
|
}(),
|
||||||
|
want: &ServiceTopology{
|
||||||
|
Protocol: "http",
|
||||||
|
Downstreams: []*ServiceTopologySummary{},
|
||||||
|
Upstreams: []*ServiceTopologySummary{
|
||||||
|
{
|
||||||
|
ServiceSummary: ServiceSummary{
|
||||||
|
Name: "routing-config",
|
||||||
|
Datacenter: "dc1",
|
||||||
|
EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(),
|
||||||
|
TransparentProxy: false,
|
||||||
|
},
|
||||||
|
Intention: structs.IntentionDecisionSummary{
|
||||||
|
DefaultAllow: true,
|
||||||
|
Allowed: true,
|
||||||
|
},
|
||||||
|
Source: structs.TopologySourceRoutingConfig,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "counting has downstream dashboard",
|
||||||
|
httpReq: func() *http.Request {
|
||||||
|
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/counting?kind=", nil)
|
||||||
|
return req
|
||||||
|
}(),
|
||||||
|
want: &ServiceTopology{
|
||||||
|
Protocol: "http",
|
||||||
|
Upstreams: []*ServiceTopologySummary{},
|
||||||
|
Downstreams: []*ServiceTopologySummary{
|
||||||
|
{
|
||||||
|
ServiceSummary: ServiceSummary{
|
||||||
|
Name: "dashboard",
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Nodes: []string{"foo"},
|
||||||
|
InstanceCount: 1,
|
||||||
|
ChecksPassing: 1,
|
||||||
|
},
|
||||||
|
Source: "proxy-registration",
|
||||||
|
Intention: structs.IntentionDecisionSummary{
|
||||||
|
Allowed: true,
|
||||||
|
DefaultAllow: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tcs {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
run(t, tc)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUIEndpoint_MetricsProxy(t *testing.T) {
|
func TestUIEndpoint_MetricsProxy(t *testing.T) {
|
||||||
|
|
Loading…
Reference in New Issue