Add topology HTTP endpoint
This commit is contained in:
parent
7c11580e93
commit
263bd9dd92
|
@ -434,7 +434,7 @@ func (s *Store) discoveryChainSources(ws memdb.WatchSet, tx ReadTxn, dc string,
|
|||
|
||||
// Only return the services that directly target the destination
|
||||
seenSource := make(map[structs.ServiceName]bool)
|
||||
for sn, _ := range seenLink {
|
||||
for sn := range seenLink {
|
||||
req := discoverychain.CompileRequest{
|
||||
ServiceName: sn.Name,
|
||||
EvaluateInNamespace: sn.NamespaceOrDefault(),
|
||||
|
|
|
@ -99,6 +99,7 @@ func init() {
|
|||
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-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/kv/", []string{"GET", "PUT", "DELETE"}, (*HTTPHandlers).KVSEndpoint)
|
||||
registerEndpoint("/v1/operator/raft/configuration", []string{"GET"}, (*HTTPHandlers).OperatorRaftConfiguration)
|
||||
|
|
|
@ -16,30 +16,51 @@ import (
|
|||
// to extract this.
|
||||
const metaExternalSource = "external-source"
|
||||
|
||||
// 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
|
||||
|
||||
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{}
|
||||
}
|
||||
|
||||
// ServiceSummary is used to summarize a service
|
||||
type ServiceSummary struct {
|
||||
Kind structs.ServiceKind `json:",omitempty"`
|
||||
Name string
|
||||
Tags []string
|
||||
Nodes []string
|
||||
InstanceCount int
|
||||
ChecksPassing int
|
||||
ChecksWarning int
|
||||
ChecksCritical int
|
||||
ExternalSources []string
|
||||
externalSourceSet map[string]struct{} // internal to track uniqueness
|
||||
GatewayConfig GatewayConfig `json:",omitempty"`
|
||||
ConnectedWithProxy bool
|
||||
ConnectedWithGateway bool
|
||||
|
||||
structs.EnterpriseMeta
|
||||
type ServiceTopology struct {
|
||||
Upstreams []*ServiceSummary
|
||||
Downstreams []*ServiceSummary
|
||||
FilteredByACLs bool
|
||||
}
|
||||
|
||||
// UINodes is used to list the nodes in a given datacenter. We return a
|
||||
|
@ -163,9 +184,39 @@ RPC:
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Generate the summary
|
||||
// TODO (gateways) (freddy) Have Internal.ServiceDump return ServiceDump instead. Need to add bexpr filtering for type.
|
||||
return summarizeServices(out.Nodes.ToServiceDump(), out.Gateways, s.agent.config, args.Datacenter), nil
|
||||
// Store the names of the gateways associated with each service
|
||||
var (
|
||||
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
|
||||
|
@ -200,17 +251,56 @@ RPC:
|
|||
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 summarizeServices(dump structs.ServiceDump, gateways structs.GatewayServices, cfg *config.RuntimeConfig, dc string) []*ServiceSummary {
|
||||
// Collect the summary information
|
||||
var services []structs.ServiceName
|
||||
summary := make(map[structs.ServiceName]*ServiceSummary)
|
||||
func (s *HTTPHandlers) UIServiceTopology(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
// Parse arguments
|
||||
args := structs.ServiceSpecificRequest{}
|
||||
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
||||
return nil, nil
|
||||
}
|
||||
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
linkedGateways := make(map[structs.ServiceName][]structs.ServiceName)
|
||||
hasProxy := make(map[structs.ServiceName]bool)
|
||||
args.ServiceName = strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/service-topology/")
|
||||
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 {
|
||||
serv, ok := summary[service]
|
||||
|
@ -223,22 +313,12 @@ func summarizeServices(dump structs.ServiceDump, gateways structs.GatewayService
|
|||
InstanceCount: 0,
|
||||
}
|
||||
summary[service] = serv
|
||||
services = append(services, service)
|
||||
}
|
||||
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 {
|
||||
if csn.GatewayService != nil {
|
||||
if cfg != nil && csn.GatewayService != nil {
|
||||
gwsvc := csn.GatewayService
|
||||
sum := getService(gwsvc.Service)
|
||||
modifySummaryForGatewayService(cfg, dc, sum, gwsvc)
|
||||
|
@ -248,15 +328,27 @@ func summarizeServices(dump structs.ServiceDump, gateways structs.GatewayService
|
|||
if csn.Service == nil {
|
||||
continue
|
||||
}
|
||||
sid := structs.NewServiceName(csn.Service.Service, &csn.Service.EnterpriseMeta)
|
||||
sum := getService(sid)
|
||||
sn := structs.NewServiceName(csn.Service.Service, &csn.Service.EnterpriseMeta)
|
||||
sum := getService(sn)
|
||||
|
||||
svc := csn.Service
|
||||
sum.Nodes = append(sum.Nodes, csn.Node.Node)
|
||||
sum.Kind = svc.Kind
|
||||
sum.Datacenter = csn.Node.Datacenter
|
||||
sum.InstanceCount += 1
|
||||
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 {
|
||||
found := false
|
||||
|
@ -266,7 +358,6 @@ func summarizeServices(dump structs.ServiceDump, gateways structs.GatewayService
|
|||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
sum.Tags = append(sum.Tags, tag)
|
||||
}
|
||||
|
@ -288,7 +379,28 @@ func summarizeServices(dump structs.ServiceDump, gateways structs.GatewayService
|
|||
}
|
||||
|
||||
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:
|
||||
sum.ChecksPassing++
|
||||
case api.HealthWarning:
|
||||
|
@ -297,34 +409,15 @@ func summarizeServices(dump structs.ServiceDump, gateways structs.GatewayService
|
|||
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 {
|
||||
return services[i].LessThan(&services[j])
|
||||
sort.Slice(resp, func(i, j int) bool {
|
||||
return resp[i].LessThan(resp[j])
|
||||
})
|
||||
|
||||
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
|
||||
return resp
|
||||
}
|
||||
|
||||
func modifySummaryForGatewayService(
|
||||
|
|
|
@ -223,6 +223,7 @@ func TestUiServices(t *testing.T) {
|
|||
Service: &structs.NodeService{
|
||||
Kind: structs.ServiceKindTypical,
|
||||
Service: "api",
|
||||
ID: "api-1",
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
},
|
||||
Checks: structs.HealthChecks{
|
||||
|
@ -230,18 +231,20 @@ func TestUiServices(t *testing.T) {
|
|||
Node: "foo",
|
||||
Name: "api svc check",
|
||||
ServiceName: "api",
|
||||
ServiceID: "api-1",
|
||||
Status: api.HealthWarning,
|
||||
},
|
||||
},
|
||||
},
|
||||
// register web svc on node foo
|
||||
// register api-proxy svc on node foo
|
||||
{
|
||||
Datacenter: "dc1",
|
||||
Node: "foo",
|
||||
SkipNodeUpdate: true,
|
||||
Service: &structs.NodeService{
|
||||
Kind: structs.ServiceKindConnectProxy,
|
||||
Service: "web",
|
||||
Service: "api-proxy",
|
||||
ID: "api-proxy-1",
|
||||
Tags: []string{},
|
||||
Meta: map[string]string{metaExternalSource: "k8s"},
|
||||
Port: 1234,
|
||||
|
@ -252,8 +255,9 @@ func TestUiServices(t *testing.T) {
|
|||
Checks: structs.HealthChecks{
|
||||
&structs.HealthCheck{
|
||||
Node: "foo",
|
||||
Name: "web svc check",
|
||||
ServiceName: "web",
|
||||
Name: "api proxy listening",
|
||||
ServiceName: "api-proxy",
|
||||
ServiceID: "api-proxy-1",
|
||||
Status: api.HealthPassing,
|
||||
},
|
||||
},
|
||||
|
@ -264,14 +268,12 @@ func TestUiServices(t *testing.T) {
|
|||
Node: "bar",
|
||||
Address: "127.0.0.2",
|
||||
Service: &structs.NodeService{
|
||||
Kind: structs.ServiceKindConnectProxy,
|
||||
Kind: structs.ServiceKindTypical,
|
||||
Service: "web",
|
||||
ID: "web-1",
|
||||
Tags: []string{},
|
||||
Meta: map[string]string{metaExternalSource: "k8s"},
|
||||
Port: 1234,
|
||||
Proxy: structs.ConnectProxyConfig{
|
||||
DestinationServiceName: "api",
|
||||
},
|
||||
},
|
||||
Checks: []*structs.HealthCheck{
|
||||
{
|
||||
|
@ -279,6 +281,7 @@ func TestUiServices(t *testing.T) {
|
|||
Name: "web svc check",
|
||||
Status: api.HealthCritical,
|
||||
ServiceName: "web",
|
||||
ServiceID: "web-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -366,76 +369,107 @@ func TestUiServices(t *testing.T) {
|
|||
assertIndex(t, resp)
|
||||
|
||||
// Should be 2 nodes, and all the empty lists should be non-nil
|
||||
summary := obj.([]*ServiceSummary)
|
||||
require.Len(t, summary, 5)
|
||||
summary := obj.([]*ServiceListingSummary)
|
||||
require.Len(t, summary, 6)
|
||||
|
||||
// internal accounting that users don't see can be blown away
|
||||
for _, sum := range summary {
|
||||
sum.externalSourceSet = nil
|
||||
sum.checks = nil
|
||||
}
|
||||
|
||||
expected := []*ServiceSummary{
|
||||
expected := []*ServiceListingSummary{
|
||||
{
|
||||
Kind: structs.ServiceKindTypical,
|
||||
Name: "api",
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
Nodes: []string{"foo"},
|
||||
InstanceCount: 1,
|
||||
ChecksPassing: 2,
|
||||
ChecksWarning: 1,
|
||||
ChecksCritical: 0,
|
||||
ServiceSummary: ServiceSummary{
|
||||
Kind: structs.ServiceKindTypical,
|
||||
Name: "api",
|
||||
Datacenter: "dc1",
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
Nodes: []string{"foo"},
|
||||
InstanceCount: 1,
|
||||
ChecksPassing: 2,
|
||||
ChecksWarning: 1,
|
||||
ChecksCritical: 0,
|
||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||
},
|
||||
ConnectedWithProxy: true,
|
||||
ConnectedWithGateway: true,
|
||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||
},
|
||||
{
|
||||
Kind: structs.ServiceKindTypical,
|
||||
Name: "cache",
|
||||
Tags: nil,
|
||||
Nodes: []string{"zip"},
|
||||
InstanceCount: 1,
|
||||
ChecksPassing: 0,
|
||||
ChecksWarning: 0,
|
||||
ChecksCritical: 0,
|
||||
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,
|
||||
Name: "cache",
|
||||
Datacenter: "dc1",
|
||||
Tags: nil,
|
||||
Nodes: []string{"zip"},
|
||||
InstanceCount: 1,
|
||||
ChecksPassing: 0,
|
||||
ChecksWarning: 0,
|
||||
ChecksCritical: 0,
|
||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||
},
|
||||
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,
|
||||
Name: "consul",
|
||||
Datacenter: "dc1",
|
||||
Tags: nil,
|
||||
Nodes: []string{a.Config.NodeName},
|
||||
InstanceCount: 1,
|
||||
ChecksPassing: 1,
|
||||
ChecksWarning: 0,
|
||||
ChecksCritical: 0,
|
||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: structs.ServiceKindTypical,
|
||||
Name: "consul",
|
||||
Tags: nil,
|
||||
Nodes: []string{a.Config.NodeName},
|
||||
InstanceCount: 1,
|
||||
ChecksPassing: 1,
|
||||
ChecksWarning: 0,
|
||||
ChecksCritical: 0,
|
||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||
ServiceSummary: ServiceSummary{
|
||||
Kind: structs.ServiceKindTerminatingGateway,
|
||||
Name: "terminating-gateway",
|
||||
Datacenter: "dc1",
|
||||
Tags: nil,
|
||||
Nodes: []string{"foo"},
|
||||
InstanceCount: 1,
|
||||
ChecksPassing: 1,
|
||||
ChecksWarning: 0,
|
||||
ChecksCritical: 0,
|
||||
GatewayConfig: GatewayConfig{AssociatedServiceCount: 2},
|
||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: structs.ServiceKindTerminatingGateway,
|
||||
Name: "terminating-gateway",
|
||||
Tags: nil,
|
||||
Nodes: []string{"foo"},
|
||||
InstanceCount: 1,
|
||||
ChecksPassing: 2,
|
||||
ChecksWarning: 1,
|
||||
GatewayConfig: GatewayConfig{AssociatedServiceCount: 2},
|
||||
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)
|
||||
})
|
||||
|
||||
|
@ -448,39 +482,46 @@ func TestUiServices(t *testing.T) {
|
|||
assertIndex(t, resp)
|
||||
|
||||
// Should be 2 nodes, and all the empty lists should be non-nil
|
||||
summary := obj.([]*ServiceSummary)
|
||||
summary := obj.([]*ServiceListingSummary)
|
||||
require.Len(t, summary, 2)
|
||||
|
||||
// internal accounting that users don't see can be blown away
|
||||
for _, sum := range summary {
|
||||
sum.externalSourceSet = nil
|
||||
sum.checks = nil
|
||||
}
|
||||
|
||||
expected := []*ServiceSummary{
|
||||
expected := []*ServiceListingSummary{
|
||||
{
|
||||
Kind: structs.ServiceKindTypical,
|
||||
Name: "api",
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
Nodes: []string{"foo"},
|
||||
InstanceCount: 1,
|
||||
ChecksPassing: 2,
|
||||
ChecksWarning: 1,
|
||||
ChecksCritical: 0,
|
||||
ConnectedWithProxy: true,
|
||||
ServiceSummary: ServiceSummary{
|
||||
Kind: structs.ServiceKindTypical,
|
||||
Name: "api",
|
||||
Datacenter: "dc1",
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
Nodes: []string{"foo"},
|
||||
InstanceCount: 1,
|
||||
ChecksPassing: 1,
|
||||
ChecksWarning: 1,
|
||||
ChecksCritical: 0,
|
||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||
},
|
||||
ConnectedWithProxy: false,
|
||||
ConnectedWithGateway: false,
|
||||
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,
|
||||
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)
|
||||
|
@ -582,7 +623,14 @@ func TestUIGatewayServiceNodes_Terminating(t *testing.T) {
|
|||
assert.Nil(t, err)
|
||||
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{
|
||||
{
|
||||
Name: "redis",
|
||||
|
@ -590,6 +638,7 @@ func TestUIGatewayServiceNodes_Terminating(t *testing.T) {
|
|||
},
|
||||
{
|
||||
Name: "db",
|
||||
Datacenter: "dc1",
|
||||
Tags: []string{"backup", "primary"},
|
||||
Nodes: []string{"bar", "baz"},
|
||||
InstanceCount: 2,
|
||||
|
@ -599,7 +648,7 @@ func TestUIGatewayServiceNodes_Terminating(t *testing.T) {
|
|||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||
},
|
||||
}
|
||||
assert.ElementsMatch(t, expect, dump)
|
||||
assert.ElementsMatch(t, expect, summary)
|
||||
}
|
||||
|
||||
func TestUIGatewayServiceNodes_Ingress(t *testing.T) {
|
||||
|
@ -748,6 +797,7 @@ func TestUIGatewayServiceNodes_Ingress(t *testing.T) {
|
|||
},
|
||||
{
|
||||
Name: "db",
|
||||
Datacenter: "dc1",
|
||||
Tags: []string{"backup", "primary"},
|
||||
Nodes: []string{"bar", "baz"},
|
||||
InstanceCount: 2,
|
||||
|
@ -767,6 +817,7 @@ func TestUIGatewayServiceNodes_Ingress(t *testing.T) {
|
|||
// internal accounting that users don't see can be blown away
|
||||
for _, sum := range dump {
|
||||
sum.GatewayConfig.addressesSet = nil
|
||||
sum.checks = nil
|
||||
}
|
||||
assert.ElementsMatch(t, expect, dump)
|
||||
}
|
||||
|
@ -878,3 +929,386 @@ func TestUIEndpoint_modifySummaryForGatewayService_UseRequestedDCInsteadOfConfig
|
|||
expected := serviceCanonicalDNSName("test", "ingress", "dc2", "consul", nil) + ":42"
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue