Add topology HTTP endpoint

This commit is contained in:
freddygv 2020-09-30 08:23:19 -06:00
parent dbbf6b2e46
commit 7c26a71b4b
4 changed files with 681 additions and 153 deletions

View File

@ -434,7 +434,7 @@ func (s *Store) discoveryChainSources(ws memdb.WatchSet, tx ReadTxn, dc string,
// Only return the services that directly target the destination // Only return the services that directly target the destination
seenSource := make(map[structs.ServiceName]bool) seenSource := make(map[structs.ServiceName]bool)
for sn, _ := range seenLink { for sn := range seenLink {
req := discoverychain.CompileRequest{ req := discoverychain.CompileRequest{
ServiceName: sn.Name, ServiceName: sn.Name,
EvaluateInNamespace: sn.NamespaceOrDefault(), EvaluateInNamespace: sn.NamespaceOrDefault(),

View File

@ -99,6 +99,7 @@ func init() {
registerEndpoint("/v1/internal/ui/services", []string{"GET"}, (*HTTPHandlers).UIServices) registerEndpoint("/v1/internal/ui/services", []string{"GET"}, (*HTTPHandlers).UIServices)
registerEndpoint("/v1/internal/ui/gateway-services-nodes/", []string{"GET"}, (*HTTPHandlers).UIGatewayServicesNodes) registerEndpoint("/v1/internal/ui/gateway-services-nodes/", []string{"GET"}, (*HTTPHandlers).UIGatewayServicesNodes)
registerEndpoint("/v1/internal/ui/gateway-intentions/", []string{"GET"}, (*HTTPHandlers).UIGatewayIntentions) registerEndpoint("/v1/internal/ui/gateway-intentions/", []string{"GET"}, (*HTTPHandlers).UIGatewayIntentions)
registerEndpoint("/v1/internal/ui/service-topology/", []string{"GET"}, (*HTTPHandlers).UIServiceTopology)
registerEndpoint("/v1/internal/acl/authorize", []string{"POST"}, (*HTTPHandlers).ACLAuthorize) registerEndpoint("/v1/internal/acl/authorize", []string{"POST"}, (*HTTPHandlers).ACLAuthorize)
registerEndpoint("/v1/kv/", []string{"GET", "PUT", "DELETE"}, (*HTTPHandlers).KVSEndpoint) registerEndpoint("/v1/kv/", []string{"GET", "PUT", "DELETE"}, (*HTTPHandlers).KVSEndpoint)
registerEndpoint("/v1/operator/raft/configuration", []string{"GET"}, (*HTTPHandlers).OperatorRaftConfiguration) registerEndpoint("/v1/operator/raft/configuration", []string{"GET"}, (*HTTPHandlers).OperatorRaftConfiguration)

View File

@ -16,32 +16,53 @@ import (
// to extract this. // to extract this.
const metaExternalSource = "external-source" const metaExternalSource = "external-source"
type GatewayConfig struct {
AssociatedServiceCount int `json:",omitempty"`
Addresses []string `json:",omitempty"`
// internal to track uniqueness
addressesSet map[string]struct{}
}
// 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"`
Name string Name string
Datacenter string
Tags []string Tags []string
Nodes []string Nodes []string
ExternalSources []string
externalSourceSet map[string]struct{} // internal to track uniqueness
checks map[string]*structs.HealthCheck
InstanceCount int InstanceCount int
ChecksPassing int ChecksPassing int
ChecksWarning int ChecksWarning int
ChecksCritical int ChecksCritical int
ExternalSources []string GatewayConfig GatewayConfig
externalSourceSet map[string]struct{} // internal to track uniqueness
GatewayConfig GatewayConfig `json:",omitempty"`
ConnectedWithProxy bool
ConnectedWithGateway bool
structs.EnterpriseMeta structs.EnterpriseMeta
} }
func (s *ServiceSummary) LessThan(other *ServiceSummary) bool {
if s.EnterpriseMeta.LessThan(&other.EnterpriseMeta) {
return true
}
return s.Name < other.Name
}
type ServiceListingSummary struct {
ServiceSummary
ConnectedWithProxy bool
ConnectedWithGateway bool
}
type GatewayConfig struct {
AssociatedServiceCount int `json:",omitempty"`
Addresses []string `json:",omitempty"`
// internal to track uniqueness
addressesSet map[string]struct{}
}
type ServiceTopology struct {
Upstreams []*ServiceSummary
Downstreams []*ServiceSummary
FilteredByACLs bool
}
// UINodes is used to list the nodes in a given datacenter. We return a // UINodes is used to list the nodes in a given datacenter. We return a
// NodeDump which provides overview information for all the nodes // NodeDump which provides overview information for all the nodes
func (s *HTTPHandlers) UINodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) { func (s *HTTPHandlers) UINodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
@ -163,9 +184,39 @@ RPC:
return nil, err return nil, err
} }
// Generate the summary // Store the names of the gateways associated with each service
// TODO (gateways) (freddy) Have Internal.ServiceDump return ServiceDump instead. Need to add bexpr filtering for type. var (
return summarizeServices(out.Nodes.ToServiceDump(), out.Gateways, s.agent.config, args.Datacenter), nil serviceGateways = make(map[structs.ServiceName][]structs.ServiceName)
numLinkedServices = make(map[structs.ServiceName]int)
)
for _, gs := range out.Gateways {
serviceGateways[gs.Service] = append(serviceGateways[gs.Service], gs.Gateway)
numLinkedServices[gs.Gateway] += 1
}
summaries, hasProxy := summarizeServices(out.Nodes.ToServiceDump(), nil, "")
sorted := prepSummaryOutput(summaries, false)
var result []*ServiceListingSummary
for _, svc := range sorted {
sum := ServiceListingSummary{ServiceSummary: *svc}
sn := structs.NewServiceName(svc.Name, &svc.EnterpriseMeta)
if hasProxy[sn] {
sum.ConnectedWithProxy = true
}
// Verify that at least one of the gateways linked by config entry has an instance registered in the catalog
for _, gw := range serviceGateways[sn] {
if s := summaries[gw]; s != nil && sum.InstanceCount > 0 {
sum.ConnectedWithGateway = true
}
}
sum.GatewayConfig.AssociatedServiceCount = numLinkedServices[sn]
result = append(result, &sum)
}
return result, nil
} }
// UIGatewayServices is used to query all the nodes for services associated with a gateway along with their gateway config // UIGatewayServices is used to query all the nodes for services associated with a gateway along with their gateway config
@ -200,17 +251,56 @@ RPC:
return nil, err return nil, err
} }
return summarizeServices(out.Dump, nil, s.agent.config, args.Datacenter), nil summaries, _ := summarizeServices(out.Dump, s.agent.config, args.Datacenter)
return prepSummaryOutput(summaries, false), nil
} }
// TODO (freddy): Refactor to split up for the two use cases func (s *HTTPHandlers) UIServiceTopology(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
func summarizeServices(dump structs.ServiceDump, gateways structs.GatewayServices, cfg *config.RuntimeConfig, dc string) []*ServiceSummary { // Parse arguments
// Collect the summary information args := structs.ServiceSpecificRequest{}
var services []structs.ServiceName if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
summary := make(map[structs.ServiceName]*ServiceSummary) return nil, nil
}
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
return nil, err
}
linkedGateways := make(map[structs.ServiceName][]structs.ServiceName) args.ServiceName = strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/service-topology/")
hasProxy := make(map[structs.ServiceName]bool) if args.ServiceName == "" {
resp.WriteHeader(http.StatusBadRequest)
fmt.Fprint(resp, "Missing service name")
return nil, nil
}
// Make the RPC request
var out structs.IndexedServiceTopology
defer setMeta(resp, &out.QueryMeta)
RPC:
if err := s.agent.RPC("Internal.ServiceTopology", &args, &out); err != nil {
// Retry the request allowing stale data if no leader
if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !args.AllowStale {
args.AllowStale = true
goto RPC
}
return nil, err
}
upstreams, _ := summarizeServices(out.ServiceTopology.Upstreams.ToServiceDump(), nil, "")
downstreams, _ := summarizeServices(out.ServiceTopology.Downstreams.ToServiceDump(), nil, "")
sum := ServiceTopology{
Upstreams: prepSummaryOutput(upstreams, true),
Downstreams: prepSummaryOutput(downstreams, true),
FilteredByACLs: out.FilteredByACLs,
}
return sum, nil
}
func summarizeServices(dump structs.ServiceDump, cfg *config.RuntimeConfig, dc string) (map[structs.ServiceName]*ServiceSummary, map[structs.ServiceName]bool) {
var (
summary = make(map[structs.ServiceName]*ServiceSummary)
hasProxy = make(map[structs.ServiceName]bool)
)
getService := func(service structs.ServiceName) *ServiceSummary { getService := func(service structs.ServiceName) *ServiceSummary {
serv, ok := summary[service] serv, ok := summary[service]
@ -223,22 +313,12 @@ func summarizeServices(dump structs.ServiceDump, gateways structs.GatewayService
InstanceCount: 0, InstanceCount: 0,
} }
summary[service] = serv summary[service] = serv
services = append(services, service)
} }
return serv return serv
} }
// Collect the list of services linked to each gateway up front
// THis also allows tracking whether a service name is associated with a gateway
gsCount := make(map[structs.ServiceName]int)
for _, gs := range gateways {
gsCount[gs.Gateway] += 1
linkedGateways[gs.Service] = append(linkedGateways[gs.Service], gs.Gateway)
}
for _, csn := range dump { for _, csn := range dump {
if csn.GatewayService != nil { if cfg != nil && csn.GatewayService != nil {
gwsvc := csn.GatewayService gwsvc := csn.GatewayService
sum := getService(gwsvc.Service) sum := getService(gwsvc.Service)
modifySummaryForGatewayService(cfg, dc, sum, gwsvc) modifySummaryForGatewayService(cfg, dc, sum, gwsvc)
@ -248,15 +328,27 @@ func summarizeServices(dump structs.ServiceDump, gateways structs.GatewayService
if csn.Service == nil { if csn.Service == nil {
continue continue
} }
sid := structs.NewServiceName(csn.Service.Service, &csn.Service.EnterpriseMeta) sn := structs.NewServiceName(csn.Service.Service, &csn.Service.EnterpriseMeta)
sum := getService(sid) sum := getService(sn)
svc := csn.Service svc := csn.Service
sum.Nodes = append(sum.Nodes, csn.Node.Node) sum.Nodes = append(sum.Nodes, csn.Node.Node)
sum.Kind = svc.Kind sum.Kind = svc.Kind
sum.Datacenter = csn.Node.Datacenter
sum.InstanceCount += 1 sum.InstanceCount += 1
if svc.Kind == structs.ServiceKindConnectProxy { if svc.Kind == structs.ServiceKindConnectProxy {
hasProxy[structs.NewServiceName(svc.Proxy.DestinationServiceName, &svc.EnterpriseMeta)] = true sn := structs.NewServiceName(svc.Proxy.DestinationServiceName, &svc.EnterpriseMeta)
hasProxy[sn] = true
destination := getService(sn)
for _, check := range csn.Checks {
cid := structs.NewCheckID(check.CheckID, &check.EnterpriseMeta)
uid := structs.UniqueID(csn.Node.Node, cid.String())
if destination.checks == nil {
destination.checks = make(map[string]*structs.HealthCheck)
}
destination.checks[uid] = check
}
} }
for _, tag := range svc.Tags { for _, tag := range svc.Tags {
found := false found := false
@ -266,7 +358,6 @@ func summarizeServices(dump structs.ServiceDump, gateways structs.GatewayService
break break
} }
} }
if !found { if !found {
sum.Tags = append(sum.Tags, tag) sum.Tags = append(sum.Tags, tag)
} }
@ -288,7 +379,28 @@ func summarizeServices(dump structs.ServiceDump, gateways structs.GatewayService
} }
for _, check := range csn.Checks { for _, check := range csn.Checks {
switch check.Status { cid := structs.NewCheckID(check.CheckID, &check.EnterpriseMeta)
uid := structs.UniqueID(csn.Node.Node, cid.String())
if sum.checks == nil {
sum.checks = make(map[string]*structs.HealthCheck)
}
sum.checks[uid] = check
}
}
return summary, hasProxy
}
func prepSummaryOutput(summaries map[structs.ServiceName]*ServiceSummary, excludeSidecars bool) []*ServiceSummary {
var resp []*ServiceSummary
// Collect and sort resp for display
for _, sum := range summaries {
sort.Strings(sum.Nodes)
sort.Strings(sum.Tags)
for _, chk := range sum.checks {
switch chk.Status {
case api.HealthPassing: case api.HealthPassing:
sum.ChecksPassing++ sum.ChecksPassing++
case api.HealthWarning: case api.HealthWarning:
@ -297,34 +409,15 @@ func summarizeServices(dump structs.ServiceDump, gateways structs.GatewayService
sum.ChecksCritical++ sum.ChecksCritical++
} }
} }
if excludeSidecars && sum.Kind != structs.ServiceKindTypical {
continue
} }
resp = append(resp, sum)
// Return the services in sorted order }
sort.Slice(services, func(i, j int) bool { sort.Slice(resp, func(i, j int) bool {
return services[i].LessThan(&services[j]) return resp[i].LessThan(resp[j])
}) })
return resp
output := make([]*ServiceSummary, len(summary))
for idx, service := range services {
sum := summary[service]
if hasProxy[service] {
sum.ConnectedWithProxy = true
}
// Verify that at least one of the gateways linked by config entry has an instance registered in the catalog
for _, gw := range linkedGateways[service] {
if s := summary[gw]; s != nil && s.InstanceCount > 0 {
sum.ConnectedWithGateway = true
}
}
sum.GatewayConfig.AssociatedServiceCount = gsCount[service]
// Sort the nodes and tags
sort.Strings(sum.Nodes)
sort.Strings(sum.Tags)
output[idx] = sum
}
return output
} }
func modifySummaryForGatewayService( func modifySummaryForGatewayService(

View File

@ -223,6 +223,7 @@ func TestUiServices(t *testing.T) {
Service: &structs.NodeService{ Service: &structs.NodeService{
Kind: structs.ServiceKindTypical, Kind: structs.ServiceKindTypical,
Service: "api", Service: "api",
ID: "api-1",
Tags: []string{"tag1", "tag2"}, Tags: []string{"tag1", "tag2"},
}, },
Checks: structs.HealthChecks{ Checks: structs.HealthChecks{
@ -230,18 +231,20 @@ func TestUiServices(t *testing.T) {
Node: "foo", Node: "foo",
Name: "api svc check", Name: "api svc check",
ServiceName: "api", ServiceName: "api",
ServiceID: "api-1",
Status: api.HealthWarning, Status: api.HealthWarning,
}, },
}, },
}, },
// register web svc on node foo // register api-proxy svc on node foo
{ {
Datacenter: "dc1", Datacenter: "dc1",
Node: "foo", Node: "foo",
SkipNodeUpdate: true, SkipNodeUpdate: true,
Service: &structs.NodeService{ Service: &structs.NodeService{
Kind: structs.ServiceKindConnectProxy, Kind: structs.ServiceKindConnectProxy,
Service: "web", Service: "api-proxy",
ID: "api-proxy-1",
Tags: []string{}, Tags: []string{},
Meta: map[string]string{metaExternalSource: "k8s"}, Meta: map[string]string{metaExternalSource: "k8s"},
Port: 1234, Port: 1234,
@ -252,8 +255,9 @@ func TestUiServices(t *testing.T) {
Checks: structs.HealthChecks{ Checks: structs.HealthChecks{
&structs.HealthCheck{ &structs.HealthCheck{
Node: "foo", Node: "foo",
Name: "web svc check", Name: "api proxy listening",
ServiceName: "web", ServiceName: "api-proxy",
ServiceID: "api-proxy-1",
Status: api.HealthPassing, Status: api.HealthPassing,
}, },
}, },
@ -264,14 +268,12 @@ func TestUiServices(t *testing.T) {
Node: "bar", Node: "bar",
Address: "127.0.0.2", Address: "127.0.0.2",
Service: &structs.NodeService{ Service: &structs.NodeService{
Kind: structs.ServiceKindConnectProxy, Kind: structs.ServiceKindTypical,
Service: "web", Service: "web",
ID: "web-1",
Tags: []string{}, Tags: []string{},
Meta: map[string]string{metaExternalSource: "k8s"}, Meta: map[string]string{metaExternalSource: "k8s"},
Port: 1234, Port: 1234,
Proxy: structs.ConnectProxyConfig{
DestinationServiceName: "api",
},
}, },
Checks: []*structs.HealthCheck{ Checks: []*structs.HealthCheck{
{ {
@ -279,6 +281,7 @@ func TestUiServices(t *testing.T) {
Name: "web svc check", Name: "web svc check",
Status: api.HealthCritical, Status: api.HealthCritical,
ServiceName: "web", ServiceName: "web",
ServiceID: "web-1",
}, },
}, },
}, },
@ -366,55 +369,67 @@ func TestUiServices(t *testing.T) {
assertIndex(t, resp) assertIndex(t, resp)
// Should be 2 nodes, and all the empty lists should be non-nil // Should be 2 nodes, and all the empty lists should be non-nil
summary := obj.([]*ServiceSummary) summary := obj.([]*ServiceListingSummary)
require.Len(t, summary, 5) require.Len(t, summary, 6)
// internal accounting that users don't see can be blown away // internal accounting that users don't see can be blown away
for _, sum := range summary { for _, sum := range summary {
sum.externalSourceSet = nil sum.externalSourceSet = nil
sum.checks = nil
} }
expected := []*ServiceSummary{ expected := []*ServiceListingSummary{
{ {
ServiceSummary: ServiceSummary{
Kind: structs.ServiceKindTypical, Kind: structs.ServiceKindTypical,
Name: "api", Name: "api",
Datacenter: "dc1",
Tags: []string{"tag1", "tag2"}, Tags: []string{"tag1", "tag2"},
Nodes: []string{"foo"}, Nodes: []string{"foo"},
InstanceCount: 1, InstanceCount: 1,
ChecksPassing: 2, ChecksPassing: 2,
ChecksWarning: 1, ChecksWarning: 1,
ChecksCritical: 0, ChecksCritical: 0,
ConnectedWithProxy: true,
ConnectedWithGateway: true,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(), EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
}, },
ConnectedWithProxy: true,
ConnectedWithGateway: true,
},
{ {
ServiceSummary: ServiceSummary{
Kind: structs.ServiceKindConnectProxy,
Name: "api-proxy",
Datacenter: "dc1",
Tags: nil,
Nodes: []string{"foo"},
InstanceCount: 1,
ChecksPassing: 2,
ChecksWarning: 0,
ChecksCritical: 0,
ExternalSources: []string{"k8s"},
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
},
{
ServiceSummary: ServiceSummary{
Kind: structs.ServiceKindTypical, Kind: structs.ServiceKindTypical,
Name: "cache", Name: "cache",
Datacenter: "dc1",
Tags: nil, Tags: nil,
Nodes: []string{"zip"}, Nodes: []string{"zip"},
InstanceCount: 1, InstanceCount: 1,
ChecksPassing: 0, ChecksPassing: 0,
ChecksWarning: 0, ChecksWarning: 0,
ChecksCritical: 0, ChecksCritical: 0,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
ConnectedWithGateway: true, ConnectedWithGateway: true,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
{
Kind: structs.ServiceKindConnectProxy,
Name: "web",
Tags: nil,
Nodes: []string{"bar", "foo"},
InstanceCount: 2,
ChecksPassing: 2,
ChecksWarning: 1,
ChecksCritical: 1,
ExternalSources: []string{"k8s"},
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
}, },
{ {
ServiceSummary: ServiceSummary{
Kind: structs.ServiceKindTypical, Kind: structs.ServiceKindTypical,
Name: "consul", Name: "consul",
Datacenter: "dc1",
Tags: nil, Tags: nil,
Nodes: []string{a.Config.NodeName}, Nodes: []string{a.Config.NodeName},
InstanceCount: 1, InstanceCount: 1,
@ -423,19 +438,38 @@ func TestUiServices(t *testing.T) {
ChecksCritical: 0, ChecksCritical: 0,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(), EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
}, },
},
{ {
ServiceSummary: ServiceSummary{
Kind: structs.ServiceKindTerminatingGateway, Kind: structs.ServiceKindTerminatingGateway,
Name: "terminating-gateway", Name: "terminating-gateway",
Datacenter: "dc1",
Tags: nil, Tags: nil,
Nodes: []string{"foo"}, Nodes: []string{"foo"},
InstanceCount: 1, InstanceCount: 1,
ChecksPassing: 2, ChecksPassing: 1,
ChecksWarning: 1, ChecksWarning: 0,
ChecksCritical: 0,
GatewayConfig: GatewayConfig{AssociatedServiceCount: 2}, GatewayConfig: GatewayConfig{AssociatedServiceCount: 2},
EnterpriseMeta: *structs.DefaultEnterpriseMeta(), EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
}, },
},
{
ServiceSummary: ServiceSummary{
Kind: structs.ServiceKindTypical,
Name: "web",
Datacenter: "dc1",
Tags: nil,
Nodes: []string{"bar"},
InstanceCount: 1,
ChecksPassing: 0,
ChecksWarning: 0,
ChecksCritical: 1,
ExternalSources: []string{"k8s"},
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
},
},
} }
require.ElementsMatch(t, expected, summary) require.ElementsMatch(t, expected, summary)
}) })
@ -448,40 +482,47 @@ func TestUiServices(t *testing.T) {
assertIndex(t, resp) assertIndex(t, resp)
// Should be 2 nodes, and all the empty lists should be non-nil // Should be 2 nodes, and all the empty lists should be non-nil
summary := obj.([]*ServiceSummary) summary := obj.([]*ServiceListingSummary)
require.Len(t, summary, 2) require.Len(t, summary, 2)
// internal accounting that users don't see can be blown away // internal accounting that users don't see can be blown away
for _, sum := range summary { for _, sum := range summary {
sum.externalSourceSet = nil sum.externalSourceSet = nil
sum.checks = nil
} }
expected := []*ServiceSummary{ expected := []*ServiceListingSummary{
{ {
ServiceSummary: ServiceSummary{
Kind: structs.ServiceKindTypical, Kind: structs.ServiceKindTypical,
Name: "api", Name: "api",
Datacenter: "dc1",
Tags: []string{"tag1", "tag2"}, Tags: []string{"tag1", "tag2"},
Nodes: []string{"foo"}, Nodes: []string{"foo"},
InstanceCount: 1, InstanceCount: 1,
ChecksPassing: 2, ChecksPassing: 1,
ChecksWarning: 1, ChecksWarning: 1,
ChecksCritical: 0, ChecksCritical: 0,
ConnectedWithProxy: true,
ConnectedWithGateway: false,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(), EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
}, },
ConnectedWithProxy: false,
ConnectedWithGateway: false,
},
{ {
Kind: structs.ServiceKindConnectProxy, ServiceSummary: ServiceSummary{
Kind: structs.ServiceKindTypical,
Name: "web", Name: "web",
Datacenter: "dc1",
Tags: nil, Tags: nil,
Nodes: []string{"bar", "foo"}, Nodes: []string{"bar"},
InstanceCount: 2, InstanceCount: 1,
ChecksPassing: 2, ChecksPassing: 0,
ChecksWarning: 1, ChecksWarning: 0,
ChecksCritical: 1, ChecksCritical: 1,
ExternalSources: []string{"k8s"}, ExternalSources: []string{"k8s"},
EnterpriseMeta: *structs.DefaultEnterpriseMeta(), EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
}, },
},
} }
require.ElementsMatch(t, expected, summary) require.ElementsMatch(t, expected, summary)
}) })
@ -582,7 +623,14 @@ func TestUIGatewayServiceNodes_Terminating(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assertIndex(t, resp) assertIndex(t, resp)
dump := obj.([]*ServiceSummary) summary := obj.([]*ServiceSummary)
// internal accounting that users don't see can be blown away
for _, sum := range summary {
sum.externalSourceSet = nil
sum.checks = nil
}
expect := []*ServiceSummary{ expect := []*ServiceSummary{
{ {
Name: "redis", Name: "redis",
@ -590,6 +638,7 @@ func TestUIGatewayServiceNodes_Terminating(t *testing.T) {
}, },
{ {
Name: "db", Name: "db",
Datacenter: "dc1",
Tags: []string{"backup", "primary"}, Tags: []string{"backup", "primary"},
Nodes: []string{"bar", "baz"}, Nodes: []string{"bar", "baz"},
InstanceCount: 2, InstanceCount: 2,
@ -599,7 +648,7 @@ func TestUIGatewayServiceNodes_Terminating(t *testing.T) {
EnterpriseMeta: *structs.DefaultEnterpriseMeta(), EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
}, },
} }
assert.ElementsMatch(t, expect, dump) assert.ElementsMatch(t, expect, summary)
} }
func TestUIGatewayServiceNodes_Ingress(t *testing.T) { func TestUIGatewayServiceNodes_Ingress(t *testing.T) {
@ -748,6 +797,7 @@ func TestUIGatewayServiceNodes_Ingress(t *testing.T) {
}, },
{ {
Name: "db", Name: "db",
Datacenter: "dc1",
Tags: []string{"backup", "primary"}, Tags: []string{"backup", "primary"},
Nodes: []string{"bar", "baz"}, Nodes: []string{"bar", "baz"},
InstanceCount: 2, InstanceCount: 2,
@ -767,6 +817,7 @@ func TestUIGatewayServiceNodes_Ingress(t *testing.T) {
// internal accounting that users don't see can be blown away // internal accounting that users don't see can be blown away
for _, sum := range dump { for _, sum := range dump {
sum.GatewayConfig.addressesSet = nil sum.GatewayConfig.addressesSet = nil
sum.checks = nil
} }
assert.ElementsMatch(t, expect, dump) assert.ElementsMatch(t, expect, dump)
} }
@ -878,3 +929,386 @@ func TestUIEndpoint_modifySummaryForGatewayService_UseRequestedDCInsteadOfConfig
expected := serviceCanonicalDNSName("test", "ingress", "dc2", "consul", nil) + ":42" expected := serviceCanonicalDNSName("test", "ingress", "dc2", "consul", nil) + ":42"
require.Equal(t, expected, sum.GatewayConfig.Addresses[0]) require.Equal(t, expected, sum.GatewayConfig.Addresses[0])
} }
func TestUIServiceTopology(t *testing.T) {
t.Parallel()
a := NewTestAgent(t, "")
defer a.Shutdown()
// Register terminating gateway and config entry linking it to postgres + redis
{
registrations := map[string]*structs.RegisterRequest{
"Node foo": {
Datacenter: "dc1",
Node: "foo",
Address: "127.0.0.2",
Checks: structs.HealthChecks{
&structs.HealthCheck{
Node: "foo",
CheckID: "foo:alive",
Name: "foo-liveness",
Status: api.HealthPassing,
},
},
},
"Service api on foo": {
Datacenter: "dc1",
Node: "foo",
SkipNodeUpdate: true,
Service: &structs.NodeService{
Kind: structs.ServiceKindTypical,
ID: "api",
Service: "api",
Port: 9090,
Address: "198.18.1.2",
},
Checks: structs.HealthChecks{
&structs.HealthCheck{
Node: "foo",
CheckID: "foo:api",
Name: "api-liveness",
Status: api.HealthPassing,
ServiceID: "api",
ServiceName: "api",
},
},
},
"Service api-proxy": {
Datacenter: "dc1",
Node: "foo",
SkipNodeUpdate: true,
Service: &structs.NodeService{
Kind: structs.ServiceKindConnectProxy,
ID: "api-proxy",
Service: "api-proxy",
Port: 8443,
Address: "198.18.1.2",
Proxy: structs.ConnectProxyConfig{
DestinationServiceName: "api",
Upstreams: structs.Upstreams{
{
DestinationName: "web",
LocalBindPort: 8080,
},
},
},
},
Checks: structs.HealthChecks{
&structs.HealthCheck{
Node: "foo",
CheckID: "foo:api-proxy",
Name: "api proxy listening",
Status: api.HealthPassing,
ServiceID: "api-proxy",
ServiceName: "api-proxy",
},
},
},
"Node bar": {
Datacenter: "dc1",
Node: "bar",
Address: "127.0.0.3",
Checks: structs.HealthChecks{
&structs.HealthCheck{
Node: "bar",
CheckID: "bar:alive",
Name: "bar-liveness",
Status: api.HealthPassing,
},
},
},
"Service web on bar": {
Datacenter: "dc1",
Node: "bar",
SkipNodeUpdate: true,
Service: &structs.NodeService{
Kind: structs.ServiceKindTypical,
ID: "web",
Service: "web",
Port: 80,
Address: "198.18.1.20",
},
Checks: structs.HealthChecks{
&structs.HealthCheck{
Node: "bar",
CheckID: "bar:web",
Name: "web-liveness",
Status: api.HealthWarning,
ServiceID: "web",
ServiceName: "web",
},
},
},
"Service web-proxy on bar": {
Datacenter: "dc1",
Node: "bar",
SkipNodeUpdate: true,
Service: &structs.NodeService{
Kind: structs.ServiceKindConnectProxy,
ID: "web-proxy",
Service: "web-proxy",
Port: 8443,
Address: "198.18.1.20",
Proxy: structs.ConnectProxyConfig{
DestinationServiceName: "web",
Upstreams: structs.Upstreams{
{
DestinationName: "redis",
LocalBindPort: 123,
},
},
},
},
Checks: structs.HealthChecks{
&structs.HealthCheck{
Node: "bar",
CheckID: "bar:web-proxy",
Name: "web proxy listening",
Status: api.HealthCritical,
ServiceID: "web-proxy",
ServiceName: "web-proxy",
},
},
},
"Node baz": {
Datacenter: "dc1",
Node: "baz",
Address: "127.0.0.4",
Checks: structs.HealthChecks{
&structs.HealthCheck{
Node: "baz",
CheckID: "baz:alive",
Name: "baz-liveness",
Status: api.HealthPassing,
},
},
},
"Service web on baz": {
Datacenter: "dc1",
Node: "baz",
SkipNodeUpdate: true,
Service: &structs.NodeService{
Kind: structs.ServiceKindTypical,
ID: "web",
Service: "web",
Port: 80,
Address: "198.18.1.40",
},
Checks: structs.HealthChecks{
&structs.HealthCheck{
Node: "baz",
CheckID: "baz:web",
Name: "web-liveness",
Status: api.HealthPassing,
ServiceID: "web",
ServiceName: "web",
},
},
},
"Service web-proxy on baz": {
Datacenter: "dc1",
Node: "baz",
SkipNodeUpdate: true,
Service: &structs.NodeService{
Kind: structs.ServiceKindConnectProxy,
ID: "web-proxy",
Service: "web-proxy",
Port: 8443,
Address: "198.18.1.40",
Proxy: structs.ConnectProxyConfig{
DestinationServiceName: "web",
Upstreams: structs.Upstreams{
{
DestinationName: "redis",
LocalBindPort: 123,
},
},
},
},
Checks: structs.HealthChecks{
&structs.HealthCheck{
Node: "baz",
CheckID: "baz:web-proxy",
Name: "web proxy listening",
Status: api.HealthCritical,
ServiceID: "web-proxy",
ServiceName: "web-proxy",
},
},
},
"Node zip": {
Datacenter: "dc1",
Node: "zip",
Address: "127.0.0.5",
Checks: structs.HealthChecks{
&structs.HealthCheck{
Node: "zip",
CheckID: "zip:alive",
Name: "zip-liveness",
Status: api.HealthPassing,
},
},
},
"Service redis on zip": {
Datacenter: "dc1",
Node: "zip",
SkipNodeUpdate: true,
Service: &structs.NodeService{
Kind: structs.ServiceKindTypical,
ID: "redis",
Service: "redis",
Port: 6379,
Address: "198.18.1.60",
},
Checks: structs.HealthChecks{
&structs.HealthCheck{
Node: "zip",
CheckID: "zip:redis",
Name: "redis-liveness",
Status: api.HealthPassing,
ServiceID: "redis",
ServiceName: "redis",
},
},
},
"Service redis-proxy on zip": {
Datacenter: "dc1",
Node: "zip",
SkipNodeUpdate: true,
Service: &structs.NodeService{
Kind: structs.ServiceKindConnectProxy,
ID: "redis-proxy",
Service: "redis-proxy",
Port: 8443,
Address: "198.18.1.60",
Proxy: structs.ConnectProxyConfig{
DestinationServiceName: "redis",
},
},
Checks: structs.HealthChecks{
&structs.HealthCheck{
Node: "zip",
CheckID: "zip:redis-proxy",
Name: "redis proxy listening",
Status: api.HealthCritical,
ServiceID: "redis-proxy",
ServiceName: "redis-proxy",
},
},
},
}
for _, args := range registrations {
var out struct{}
require.NoError(t, a.RPC("Catalog.Register", args, &out))
}
}
t.Run("api", func(t *testing.T) {
// Request topology for api
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/api", nil)
resp := httptest.NewRecorder()
obj, err := a.srv.UIServiceTopology(resp, req)
assert.Nil(t, err)
assertIndex(t, resp)
expect := ServiceTopology{
Upstreams: []*ServiceSummary{
{
Name: "web",
Datacenter: "dc1",
Nodes: []string{"bar", "baz"},
InstanceCount: 2,
ChecksPassing: 3,
ChecksWarning: 1,
ChecksCritical: 2,
},
},
FilteredByACLs: false,
}
result := obj.(ServiceTopology)
// Internal accounting that is not returned in JSON response
for _, u := range result.Upstreams {
u.externalSourceSet = nil
u.checks = nil
}
require.Equal(t, expect, result)
})
t.Run("web", func(t *testing.T) {
// Request topology for web
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/web", nil)
resp := httptest.NewRecorder()
obj, err := a.srv.UIServiceTopology(resp, req)
assert.Nil(t, err)
assertIndex(t, resp)
expect := ServiceTopology{
Upstreams: []*ServiceSummary{
{
Name: "redis",
Datacenter: "dc1",
Nodes: []string{"zip"},
InstanceCount: 1,
ChecksPassing: 2,
ChecksCritical: 1,
},
},
Downstreams: []*ServiceSummary{
{
Name: "api",
Datacenter: "dc1",
Nodes: []string{"foo"},
InstanceCount: 1,
ChecksPassing: 3,
},
},
FilteredByACLs: false,
}
result := obj.(ServiceTopology)
// Internal accounting that is not returned in JSON response
for _, u := range result.Upstreams {
u.externalSourceSet = nil
u.checks = nil
}
for _, d := range result.Downstreams {
d.externalSourceSet = nil
d.checks = nil
}
require.Equal(t, expect, result)
})
t.Run("redis", func(t *testing.T) {
// Request topology for redis
req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/redis", nil)
resp := httptest.NewRecorder()
obj, err := a.srv.UIServiceTopology(resp, req)
assert.Nil(t, err)
assertIndex(t, resp)
expect := ServiceTopology{
Downstreams: []*ServiceSummary{
{
Name: "web",
Datacenter: "dc1",
Nodes: []string{"bar", "baz"},
InstanceCount: 2,
ChecksPassing: 3,
ChecksWarning: 1,
ChecksCritical: 2,
},
},
FilteredByACLs: false,
}
result := obj.(ServiceTopology)
// Internal accounting that is not returned in JSON response
for _, d := range result.Downstreams {
d.externalSourceSet = nil
d.checks = nil
}
require.Equal(t, expect, result)
})
}