946 lines
30 KiB
Go
946 lines
30 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package agent
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/serf/serf"
|
|
|
|
"github.com/hashicorp/consul/acl"
|
|
"github.com/hashicorp/consul/agent/config"
|
|
"github.com/hashicorp/consul/agent/consul"
|
|
"github.com/hashicorp/consul/agent/metadata"
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
"github.com/hashicorp/consul/api"
|
|
"github.com/hashicorp/consul/logging"
|
|
)
|
|
|
|
// 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
|
|
TransparentProxy bool
|
|
transparentProxySet bool
|
|
ConnectNative bool
|
|
|
|
PeerName string `json:",omitempty"`
|
|
|
|
acl.EnterpriseMeta
|
|
}
|
|
|
|
func (s *ServiceSummary) LessThan(other *ServiceSummary) bool {
|
|
if s.EnterpriseMeta.LessThan(&other.EnterpriseMeta) {
|
|
return true
|
|
}
|
|
return s.Name < other.Name
|
|
}
|
|
|
|
type GatewayConfig struct {
|
|
AssociatedServiceCount int `json:",omitempty"`
|
|
Addresses []string `json:",omitempty"`
|
|
|
|
// internal to track uniqueness
|
|
addressesSet map[string]struct{}
|
|
}
|
|
|
|
type ServiceListingSummary struct {
|
|
ServiceSummary
|
|
|
|
ConnectedWithProxy bool
|
|
ConnectedWithGateway bool
|
|
}
|
|
|
|
type ServiceTopologySummary struct {
|
|
ServiceSummary
|
|
|
|
Source string
|
|
Intention structs.IntentionDecisionSummary
|
|
}
|
|
|
|
type ServiceTopology struct {
|
|
Protocol string
|
|
TransparentProxy bool
|
|
Upstreams []*ServiceTopologySummary
|
|
Downstreams []*ServiceTopologySummary
|
|
FilteredByACLs bool
|
|
}
|
|
|
|
// UINodes is used to list the nodes in a given datacenter. We return a
|
|
// NodeDump which provides overview information for all the nodes
|
|
func (s *HTTPHandlers) UINodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
// Parse arguments
|
|
args := structs.DCSpecificRequest{}
|
|
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
|
|
}
|
|
|
|
s.parseFilter(req, &args.Filter)
|
|
|
|
// Make the RPC request
|
|
var out structs.IndexedNodeDump
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
RPC:
|
|
if err := s.agent.RPC(req.Context(), "Internal.NodeDump", &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
|
|
}
|
|
|
|
// Get version info for all serf members into a map of key-address,value-version.
|
|
// This logic of calling 'AgentMembersMapAddrVer()' and inserting version info in this func
|
|
// can be discarded in future releases ( may be after 3 or 4 minor releases),
|
|
// when all the nodes are registered with consul-version in nodemeta.
|
|
var err error
|
|
mapAddrVer, err := AgentMembersMapAddrVer(s, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Use empty list instead of nil
|
|
// Also check if consul-version exists in Meta, else add it
|
|
for _, info := range out.Dump {
|
|
if info.Services == nil {
|
|
info.Services = make([]*structs.NodeService, 0)
|
|
}
|
|
if info.Checks == nil {
|
|
info.Checks = make([]*structs.HealthCheck, 0)
|
|
}
|
|
// Check if Node Meta - 'consul-version' already exists by virtue of adding
|
|
// 'consul-version' during node registration itself.
|
|
// If not, get it from mapAddrVer.
|
|
if _, ok := info.Meta[structs.MetaConsulVersion]; !ok {
|
|
if _, okver := mapAddrVer[info.Address]; okver {
|
|
if info.Meta == nil {
|
|
info.Meta = make(map[string]string)
|
|
}
|
|
info.Meta[structs.MetaConsulVersion] = mapAddrVer[info.Address]
|
|
}
|
|
}
|
|
}
|
|
if out.Dump == nil {
|
|
out.Dump = make(structs.NodeDump, 0)
|
|
}
|
|
|
|
// Use empty list instead of nil
|
|
// Also check if consul-version exists in Meta, else add it
|
|
for _, info := range out.ImportedDump {
|
|
if info.Services == nil {
|
|
info.Services = make([]*structs.NodeService, 0)
|
|
}
|
|
if info.Checks == nil {
|
|
info.Checks = make([]*structs.HealthCheck, 0)
|
|
}
|
|
// Check if Node Meta - 'consul-version' already exists by virtue of adding
|
|
// 'consul-version' during node registration itself.
|
|
// If not, get it from mapAddrVer.
|
|
if _, ok := info.Meta[structs.MetaConsulVersion]; !ok {
|
|
if _, okver := mapAddrVer[info.Address]; okver {
|
|
if info.Meta == nil {
|
|
info.Meta = make(map[string]string)
|
|
}
|
|
info.Meta[structs.MetaConsulVersion] = mapAddrVer[info.Address]
|
|
}
|
|
}
|
|
}
|
|
|
|
return append(out.Dump, out.ImportedDump...), nil
|
|
}
|
|
|
|
// AgentMembersMapAddrVer is used to get version info from all serf members into a
|
|
// map of key-address,value-version.
|
|
func AgentMembersMapAddrVer(s *HTTPHandlers, req *http.Request) (map[string]string, error) {
|
|
var members []serf.Member
|
|
|
|
//Get WAN Members
|
|
wanMembers := s.agent.WANMembers()
|
|
|
|
//Get LAN Members
|
|
//Get the request partition and default to that of the agent.
|
|
entMeta := s.agent.AgentEnterpriseMeta()
|
|
if err := s.parseEntMetaPartition(req, entMeta); err != nil {
|
|
return nil, err
|
|
}
|
|
filter := consul.LANMemberFilter{
|
|
Partition: entMeta.PartitionOrDefault(),
|
|
}
|
|
|
|
if acl.IsDefaultPartition(filter.Partition) {
|
|
filter.AllSegments = true
|
|
}
|
|
|
|
lanMembers, err := s.agent.delegate.LANMembers(filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
//aggregate members
|
|
members = append(wanMembers, lanMembers...)
|
|
|
|
//create a map with key as IPv4 address and value as consul-version
|
|
mapAddrVer := make(map[string]string, len(members))
|
|
for i := range members {
|
|
buildVersion, err := metadata.Build(&members[i])
|
|
if err == nil {
|
|
mapAddrVer[members[i].Addr.String()] = buildVersion.String()
|
|
}
|
|
}
|
|
|
|
return mapAddrVer, nil
|
|
}
|
|
|
|
// UINodeInfo is used to get info on a single node in a given datacenter. We return a
|
|
// NodeInfo which provides overview information for the node
|
|
func (s *HTTPHandlers) UINodeInfo(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
// Parse arguments
|
|
args := structs.NodeSpecificRequest{}
|
|
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
|
|
}
|
|
|
|
// Verify we have some DC, or use the default
|
|
args.Node = strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/node/")
|
|
if args.Node == "" {
|
|
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing node name"}
|
|
}
|
|
|
|
if peer := req.URL.Query().Get("peer"); peer != "" {
|
|
args.PeerName = peer
|
|
}
|
|
|
|
// Make the RPC request
|
|
var out structs.IndexedNodeDump
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
RPC:
|
|
if err := s.agent.RPC(req.Context(), "Internal.NodeInfo", &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
|
|
}
|
|
|
|
// Get version info for all serf members into a map of key-address,value-version.
|
|
// This logic of calling 'AgentMembersMapAddrVer()' and inserting version info in this func
|
|
// can be discarded in future releases ( may be after 3 or 4 minor releases),
|
|
// when all the nodes are registered with consul-version in nodemeta.
|
|
var err error
|
|
mapAddrVer, err := AgentMembersMapAddrVer(s, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Return only the first entry
|
|
if len(out.Dump) > 0 {
|
|
info := out.Dump[0]
|
|
if info.Services == nil {
|
|
info.Services = make([]*structs.NodeService, 0)
|
|
}
|
|
if info.Checks == nil {
|
|
info.Checks = make([]*structs.HealthCheck, 0)
|
|
}
|
|
// Check if Node Meta - 'consul-version' already exists by virtue of adding
|
|
// 'consul-version' during node registration itself.
|
|
// If not, get it from mapAddrVer.
|
|
if _, ok := info.Meta[structs.MetaConsulVersion]; !ok {
|
|
if _, okver := mapAddrVer[info.Address]; okver {
|
|
if info.Meta == nil {
|
|
info.Meta = make(map[string]string)
|
|
}
|
|
info.Meta[structs.MetaConsulVersion] = mapAddrVer[info.Address]
|
|
}
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
resp.WriteHeader(http.StatusNotFound)
|
|
return nil, nil
|
|
}
|
|
|
|
// UICatalogOverview is used to get a high-level overview of the health of nodes, services,
|
|
// and checks in the datacenter.
|
|
func (s *HTTPHandlers) UICatalogOverview(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
// Parse arguments
|
|
args := structs.DCSpecificRequest{}
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
return nil, nil
|
|
}
|
|
|
|
// Make the RPC request
|
|
var out structs.CatalogSummary
|
|
if err := s.agent.RPC(req.Context(), "Internal.CatalogOverview", &args, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// UIServices is used to list the services in a given datacenter. We return a
|
|
// ServiceSummary which provides overview information for the service
|
|
func (s *HTTPHandlers) UIServices(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
// Parse arguments
|
|
args := structs.ServiceDumpRequest{}
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
return nil, nil
|
|
}
|
|
if peer := req.URL.Query().Get("peer"); peer != "" {
|
|
args.PeerName = peer
|
|
}
|
|
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s.parseFilter(req, &args.Filter)
|
|
|
|
// Make the RPC request
|
|
var out structs.IndexedNodesWithGateways
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
RPC:
|
|
if err := s.agent.RPC(req.Context(), "Internal.ServiceDump", &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
|
|
}
|
|
|
|
// Store the names of the gateways associated with each service
|
|
var (
|
|
serviceGateways = make(map[structs.PeeredServiceName][]structs.PeeredServiceName)
|
|
numLinkedServices = make(map[structs.PeeredServiceName]int)
|
|
)
|
|
for _, gs := range out.Gateways {
|
|
psn := structs.PeeredServiceName{Peer: structs.DefaultPeerKeyword, ServiceName: gs.Service}
|
|
gpsn := structs.PeeredServiceName{Peer: structs.DefaultPeerKeyword, ServiceName: gs.Gateway}
|
|
serviceGateways[psn] = append(serviceGateways[psn], gpsn)
|
|
numLinkedServices[gpsn] += 1
|
|
}
|
|
|
|
summaries, hasProxy := summarizeServices(append(out.Nodes, out.ImportedNodes...).ToServiceDump(), nil, "")
|
|
sorted := prepSummaryOutput(summaries, false)
|
|
|
|
// Ensure at least a zero length slice
|
|
result := make([]*ServiceListingSummary, 0)
|
|
for _, svc := range sorted {
|
|
sum := ServiceListingSummary{ServiceSummary: *svc}
|
|
|
|
sn := structs.NewServiceName(svc.Name, &svc.EnterpriseMeta)
|
|
psn := structs.PeeredServiceName{Peer: svc.PeerName, ServiceName: sn}
|
|
if hasProxy[psn] {
|
|
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[psn] {
|
|
if s := summaries[gw]; s != nil && sum.InstanceCount > 0 {
|
|
sum.ConnectedWithGateway = true
|
|
}
|
|
}
|
|
sum.GatewayConfig.AssociatedServiceCount = numLinkedServices[psn]
|
|
|
|
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
|
|
func (s *HTTPHandlers) UIGatewayServicesNodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
// Parse arguments
|
|
args := structs.ServiceSpecificRequest{}
|
|
if err := s.parseEntMetaNoWildcard(req, &args.EnterpriseMeta); err != nil {
|
|
return nil, err
|
|
}
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
return nil, nil
|
|
}
|
|
|
|
// Pull out the service name
|
|
args.ServiceName = strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/gateway-services-nodes/")
|
|
if args.ServiceName == "" {
|
|
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing gateway name"}
|
|
}
|
|
|
|
// Make the RPC request
|
|
var out structs.IndexedServiceDump
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
RPC:
|
|
if err := s.agent.RPC(req.Context(), "Internal.GatewayServiceDump", &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
|
|
}
|
|
|
|
summaries, _ := summarizeServices(out.Dump, s.agent.config, args.Datacenter)
|
|
|
|
prepped := prepSummaryOutput(summaries, false)
|
|
if prepped == nil {
|
|
prepped = make([]*ServiceSummary, 0)
|
|
}
|
|
return prepped, nil
|
|
}
|
|
|
|
// UIServiceTopology returns the list of upstreams and downstreams for a Connect enabled service.
|
|
// - Downstreams are services that list the given service as an upstream
|
|
// - Upstreams are the upstreams defined in the given service's proxy registrations
|
|
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
|
|
}
|
|
|
|
args.ServiceName = strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/service-topology/")
|
|
if args.ServiceName == "" {
|
|
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing service name"}
|
|
}
|
|
|
|
kind, ok := req.URL.Query()["kind"]
|
|
if !ok {
|
|
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing service kind"}
|
|
}
|
|
args.ServiceKind = structs.ServiceKind(kind[0])
|
|
|
|
switch args.ServiceKind {
|
|
case structs.ServiceKindTypical, structs.ServiceKindIngressGateway:
|
|
// allowed
|
|
default:
|
|
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Unsupported service kind %q", args.ServiceKind)}
|
|
}
|
|
|
|
// Make the RPC request
|
|
var out structs.IndexedServiceTopology
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
RPC:
|
|
if err := s.agent.RPC(req.Context(), "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, "")
|
|
|
|
var (
|
|
upstreamResp = make([]*ServiceTopologySummary, 0)
|
|
downstreamResp = make([]*ServiceTopologySummary, 0)
|
|
)
|
|
|
|
// Sort and attach intention data for upstreams and downstreams
|
|
sortedUpstreams := prepSummaryOutput(upstreams, true)
|
|
for _, svc := range sortedUpstreams {
|
|
sn := structs.NewServiceName(svc.Name, &svc.EnterpriseMeta)
|
|
sum := ServiceTopologySummary{
|
|
ServiceSummary: *svc,
|
|
Intention: out.ServiceTopology.UpstreamDecisions[sn.String()],
|
|
Source: out.ServiceTopology.UpstreamSources[sn.String()],
|
|
}
|
|
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)
|
|
for _, svc := range sortedDownstreams {
|
|
sn := structs.NewServiceName(svc.Name, &svc.EnterpriseMeta)
|
|
sum := ServiceTopologySummary{
|
|
ServiceSummary: *svc,
|
|
Intention: out.ServiceTopology.DownstreamDecisions[sn.String()],
|
|
Source: out.ServiceTopology.DownstreamSources[sn.String()],
|
|
}
|
|
downstreamResp = append(downstreamResp, &sum)
|
|
}
|
|
|
|
topo := ServiceTopology{
|
|
TransparentProxy: out.ServiceTopology.TransparentProxy,
|
|
Protocol: out.ServiceTopology.MetricsProtocol,
|
|
Upstreams: upstreamResp,
|
|
Downstreams: downstreamResp,
|
|
FilteredByACLs: out.FilteredByACLs,
|
|
}
|
|
return topo, nil
|
|
}
|
|
|
|
func summarizeServices(dump structs.ServiceDump, cfg *config.RuntimeConfig, dc string) (map[structs.PeeredServiceName]*ServiceSummary, map[structs.PeeredServiceName]bool) {
|
|
var (
|
|
summary = make(map[structs.PeeredServiceName]*ServiceSummary)
|
|
hasProxy = make(map[structs.PeeredServiceName]bool)
|
|
)
|
|
|
|
getService := func(psn structs.PeeredServiceName) *ServiceSummary {
|
|
serv, ok := summary[psn]
|
|
if !ok {
|
|
serv = &ServiceSummary{
|
|
Name: psn.ServiceName.Name,
|
|
EnterpriseMeta: psn.ServiceName.EnterpriseMeta,
|
|
// the other code will increment this unconditionally so we
|
|
// shouldn't initialize it to 1
|
|
InstanceCount: 0,
|
|
PeerName: psn.Peer,
|
|
}
|
|
summary[psn] = serv
|
|
}
|
|
return serv
|
|
}
|
|
|
|
for _, csn := range dump {
|
|
var peerName string
|
|
// all entities will have the same peer name so it is safe to use the node's peer name
|
|
if csn.Node == nil {
|
|
// this can happen for gateway dumps that call this summarize func
|
|
peerName = structs.DefaultPeerKeyword
|
|
} else {
|
|
peerName = csn.Node.PeerName
|
|
}
|
|
|
|
if cfg != nil && csn.GatewayService != nil {
|
|
gwsvc := csn.GatewayService
|
|
|
|
psn := structs.PeeredServiceName{Peer: peerName, ServiceName: gwsvc.Service}
|
|
sum := getService(psn)
|
|
modifySummaryForGatewayService(cfg, dc, sum, gwsvc)
|
|
}
|
|
|
|
// Will happen in cases where we only have the GatewayServices mapping
|
|
if csn.Service == nil {
|
|
continue
|
|
}
|
|
|
|
sn := structs.NewServiceName(csn.Service.Service, &csn.Service.EnterpriseMeta)
|
|
psn := structs.PeeredServiceName{Peer: peerName, ServiceName: sn}
|
|
sum := getService(psn)
|
|
|
|
svc := csn.Service
|
|
|
|
found := false
|
|
for _, existing := range sum.Nodes {
|
|
if existing == csn.Node.Node {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
sum.Nodes = append(sum.Nodes, csn.Node.Node)
|
|
}
|
|
|
|
sum.Kind = svc.Kind
|
|
sum.Datacenter = csn.Node.Datacenter
|
|
sum.InstanceCount += 1
|
|
// Consider a service connect native once at least one instance is
|
|
if svc.Connect.Native {
|
|
sum.ConnectNative = svc.Connect.Native
|
|
}
|
|
if svc.Kind == structs.ServiceKindConnectProxy {
|
|
sn := structs.NewServiceName(svc.Proxy.DestinationServiceName, &svc.EnterpriseMeta)
|
|
psn := structs.PeeredServiceName{Peer: peerName, ServiceName: sn}
|
|
hasProxy[psn] = true
|
|
|
|
destination := getService(psn)
|
|
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
|
|
}
|
|
|
|
// Only consider the target service to be transparent when all its proxy instances are in that mode.
|
|
// This is done because the flag is used to display warnings about proxies needing to enable
|
|
// transparent proxy mode. If ANY instance isn't in the right mode then the warming applies.
|
|
if svc.Proxy.Mode == structs.ProxyModeTransparent && !destination.transparentProxySet {
|
|
destination.TransparentProxy = true
|
|
}
|
|
if svc.Proxy.Mode != structs.ProxyModeTransparent {
|
|
destination.TransparentProxy = false
|
|
}
|
|
destination.transparentProxySet = true
|
|
}
|
|
for _, tag := range svc.Tags {
|
|
found := false
|
|
for _, existing := range sum.Tags {
|
|
if existing == tag {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
sum.Tags = append(sum.Tags, tag)
|
|
}
|
|
}
|
|
|
|
// If there is an external source, add it to the list of external
|
|
// sources. We only want to add unique sources so there is extra
|
|
// accounting here with an unexported field to maintain the set
|
|
// of sources.
|
|
if len(svc.Meta) > 0 && svc.Meta[structs.MetaExternalSource] != "" {
|
|
source := svc.Meta[structs.MetaExternalSource]
|
|
if sum.externalSourceSet == nil {
|
|
sum.externalSourceSet = make(map[string]struct{})
|
|
}
|
|
if _, ok := sum.externalSourceSet[source]; !ok {
|
|
sum.externalSourceSet[source] = struct{}{}
|
|
sum.ExternalSources = append(sum.ExternalSources, source)
|
|
}
|
|
}
|
|
|
|
for _, check := range csn.Checks {
|
|
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.PeeredServiceName]*ServiceSummary, excludeSidecars bool) []*ServiceSummary {
|
|
var resp []*ServiceSummary
|
|
// Ensure at least a zero length slice
|
|
resp = make([]*ServiceSummary, 0)
|
|
|
|
// 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:
|
|
sum.ChecksWarning++
|
|
case api.HealthCritical:
|
|
sum.ChecksCritical++
|
|
}
|
|
}
|
|
if excludeSidecars && sum.Kind != structs.ServiceKindTypical && sum.Kind != structs.ServiceKindIngressGateway {
|
|
continue
|
|
}
|
|
resp = append(resp, sum)
|
|
}
|
|
sort.Slice(resp, func(i, j int) bool {
|
|
return resp[i].LessThan(resp[j])
|
|
})
|
|
return resp
|
|
}
|
|
|
|
func modifySummaryForGatewayService(
|
|
cfg *config.RuntimeConfig,
|
|
datacenter string,
|
|
sum *ServiceSummary,
|
|
gwsvc *structs.GatewayService,
|
|
) {
|
|
var dnsAddresses []string
|
|
for _, domain := range []string{cfg.DNSDomain, cfg.DNSAltDomain} {
|
|
// If the domain is empty, do not use it to construct a valid DNS
|
|
// address
|
|
if domain == "" {
|
|
continue
|
|
}
|
|
dnsAddresses = append(dnsAddresses, serviceIngressDNSName(
|
|
gwsvc.Service.Name,
|
|
datacenter,
|
|
domain,
|
|
&gwsvc.Service.EnterpriseMeta,
|
|
))
|
|
}
|
|
|
|
for _, addr := range gwsvc.Addresses(dnsAddresses) {
|
|
// check for duplicates, a service will have a ServiceInfo struct for
|
|
// every instance that is registered.
|
|
if _, ok := sum.GatewayConfig.addressesSet[addr]; !ok {
|
|
if sum.GatewayConfig.addressesSet == nil {
|
|
sum.GatewayConfig.addressesSet = make(map[string]struct{})
|
|
}
|
|
sum.GatewayConfig.addressesSet[addr] = struct{}{}
|
|
sum.GatewayConfig.Addresses = append(
|
|
sum.GatewayConfig.Addresses, addr,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// GET /v1/internal/ui/gateway-intentions/:gateway
|
|
func (s *HTTPHandlers) UIGatewayIntentions(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
var args structs.IntentionQueryRequest
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
return nil, nil
|
|
}
|
|
|
|
var entMeta acl.EnterpriseMeta
|
|
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Pull out the service name
|
|
name := strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/gateway-intentions/")
|
|
if name == "" {
|
|
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing gateway name"}
|
|
}
|
|
args.Match = &structs.IntentionQueryMatch{
|
|
Type: structs.IntentionMatchDestination,
|
|
Entries: []structs.IntentionMatchEntry{
|
|
{
|
|
Namespace: entMeta.NamespaceOrEmpty(),
|
|
Partition: entMeta.PartitionOrDefault(),
|
|
Name: name,
|
|
},
|
|
},
|
|
}
|
|
|
|
var reply structs.IndexedIntentions
|
|
|
|
defer setMeta(resp, &reply.QueryMeta)
|
|
if err := s.agent.RPC(req.Context(), "Internal.GatewayIntentions", args, &reply); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return reply.Intentions, nil
|
|
}
|
|
|
|
// UIMetricsProxy handles the /v1/internal/ui/metrics-proxy/ endpoint which, if
|
|
// configured, provides a simple read-only HTTP proxy to a single metrics
|
|
// backend to expose it to the UI.
|
|
func (s *HTTPHandlers) UIMetricsProxy(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
// Check the UI was enabled at agent startup (note this is not reloadable
|
|
// currently).
|
|
if !s.IsUIEnabled() {
|
|
return nil, HTTPError{StatusCode: http.StatusNotFound, Reason: "UI is not enabled"}
|
|
}
|
|
|
|
// Load reloadable proxy config
|
|
cfg, ok := s.metricsProxyCfg.Load().(config.UIMetricsProxy)
|
|
if !ok || cfg.BaseURL == "" {
|
|
// Proxy not configured
|
|
return nil, HTTPError{StatusCode: http.StatusNotFound, Reason: "Metrics proxy is not enabled"}
|
|
}
|
|
|
|
// Fetch the ACL token, if provided, but ONLY from headers since other
|
|
// metrics proxies might use a ?token query string parameter for something.
|
|
var token string
|
|
s.parseTokenFromHeaders(req, &token)
|
|
|
|
// Clear the token from the headers so we don't end up proxying it.
|
|
s.clearTokenFromHeaders(req)
|
|
|
|
var entMeta acl.EnterpriseMeta
|
|
if err := s.parseEntMetaPartition(req, &entMeta); err != nil {
|
|
return nil, err
|
|
}
|
|
authz, err := s.agent.delegate.ResolveTokenAndDefaultMeta(token, &entMeta, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// This endpoint requires wildcard read on all services and all nodes.
|
|
//
|
|
// In enterprise it requires this _in all namespaces_ too.
|
|
//
|
|
// In enterprise it requires this _in all namespaces and partitions_ too.
|
|
var authzContext acl.AuthorizerContext
|
|
wildcardEntMeta := structs.WildcardEnterpriseMetaInPartition(structs.WildcardSpecifier)
|
|
wildcardEntMeta.FillAuthzContext(&authzContext)
|
|
|
|
if err := authz.ToAllowAuthorizer().NodeReadAllAllowed(&authzContext); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := authz.ToAllowAuthorizer().ServiceReadAllAllowed(&authzContext); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log := s.agent.logger.Named(logging.UIMetricsProxy)
|
|
|
|
// Construct the new URL from the path and the base path. Note we do this here
|
|
// not in the Director function below because we can handle any errors cleanly
|
|
// here.
|
|
|
|
// Replace prefix in the path
|
|
subPath := strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/metrics-proxy")
|
|
|
|
// Append that to the BaseURL (which might contain a path prefix component)
|
|
newURL := cfg.BaseURL + subPath
|
|
|
|
// Parse it into a new URL
|
|
u, err := url.Parse(newURL)
|
|
if err != nil {
|
|
log.Error("couldn't parse target URL", "base_url", cfg.BaseURL, "path", subPath)
|
|
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Invalid path."}
|
|
}
|
|
|
|
// Clean the new URL path to prevent path traversal attacks and remove any
|
|
// double slashes etc.
|
|
u.Path = path.Clean(u.Path)
|
|
|
|
if len(cfg.PathAllowlist) > 0 {
|
|
// This could be done better with a map, but for the prometheus default
|
|
// integration this list has two items in it, so the straight iteration
|
|
// isn't awful.
|
|
denied := true
|
|
for _, allowedPath := range cfg.PathAllowlist {
|
|
if u.Path == allowedPath {
|
|
denied = false
|
|
break
|
|
}
|
|
}
|
|
if denied {
|
|
log.Error("target URL path is not allowed",
|
|
"base_url", cfg.BaseURL,
|
|
"path", subPath,
|
|
"target_url", u.String(),
|
|
"path_allowlist", cfg.PathAllowlist,
|
|
)
|
|
resp.WriteHeader(http.StatusForbidden)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
// Pass through query params
|
|
u.RawQuery = req.URL.RawQuery
|
|
|
|
// Validate that the full BaseURL is still a prefix - if there was a path
|
|
// prefix on the BaseURL but an attacker tried to circumvent it with path
|
|
// traversal then the Clean above would have resolve the /../ components back
|
|
// to the actual path which means part of the prefix will now be missing.
|
|
//
|
|
// Note that in practice this is not currently possible since any /../ in the
|
|
// path would have already been resolved by the API server mux and so not even
|
|
// hit this handler. Any /../ that are far enough into the path to hit this
|
|
// handler, can't backtrack far enough to eat into the BaseURL either. But we
|
|
// leave this in anyway in case something changes in the future.
|
|
if !strings.HasPrefix(u.String(), cfg.BaseURL) {
|
|
log.Error("target URL escaped from base path",
|
|
"base_url", cfg.BaseURL,
|
|
"path", subPath,
|
|
"target_url", u.String(),
|
|
)
|
|
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Invalid path."}
|
|
}
|
|
|
|
// Add any configured headers
|
|
for _, h := range cfg.AddHeaders {
|
|
if strings.ToLower(h.Name) == "host" {
|
|
req.Host = h.Value
|
|
} else {
|
|
req.Header.Set(h.Name, h.Value)
|
|
}
|
|
}
|
|
|
|
log.Debug("proxying request", "to", u.String())
|
|
|
|
proxy := httputil.ReverseProxy{
|
|
Director: func(r *http.Request) {
|
|
r.URL = u
|
|
},
|
|
Transport: s.proxyTransport,
|
|
ErrorLog: log.StandardLogger(&hclog.StandardLoggerOptions{
|
|
InferLevels: true,
|
|
}),
|
|
}
|
|
|
|
proxy.ServeHTTP(resp, req)
|
|
return nil, nil
|
|
}
|
|
|
|
// UIExportedServices is used to list the exported services to a given peer. We return a
|
|
// barebones ServiceListingSummary which only contains the name and enterprise meta of a service.
|
|
// Currently, the request and response mirror UIServices but the API may change in the future.
|
|
func (s *HTTPHandlers) UIExportedServices(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
// Parse arguments
|
|
args := structs.ServiceDumpRequest{}
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
return nil, nil
|
|
}
|
|
if peer := req.URL.Query().Get("peer"); peer != "" {
|
|
args.PeerName = peer
|
|
}
|
|
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Make the RPC request
|
|
var out structs.IndexedServiceList
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
RPC:
|
|
if err := s.agent.RPC(req.Context(), "Internal.ExportedServicesForPeer", &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
|
|
}
|
|
// Ensure at least a zero length slice
|
|
result := make([]*ServiceListingSummary, 0)
|
|
for _, svc := range out.Services {
|
|
// We synthesize a minimal summary for the frontend.
|
|
// The shape of the data may change in the future but
|
|
// currently only the service name is required.
|
|
sum := ServiceListingSummary{
|
|
ServiceSummary: ServiceSummary{
|
|
Name: svc.Name,
|
|
EnterpriseMeta: svc.EnterpriseMeta,
|
|
Datacenter: args.Datacenter,
|
|
},
|
|
}
|
|
result = append(result, &sum)
|
|
}
|
|
return result, nil
|
|
}
|