// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package agent
import (
Use fmt.Fprint/Fprintf/Fprintln
Used the following rewrite rules:
gofmt -w -r 'resp.Write([]byte(fmt.Sprintf(a, b, c, d))) -> fmt.Fprintf(resp, a, b, c, d)' *.go
gofmt -w -r 'resp.Write([]byte(fmt.Sprintf(a, b, c))) -> fmt.Fprintf(resp, a, b, c)' *.go
gofmt -w -r 'resp.Write([]byte(fmt.Sprintf(a, b))) -> fmt.Fprintf(resp, a, b)' *.go
gofmt -w -r 'resp.Write([]byte(fmt.Sprintf(a))) -> fmt.Fprint(resp, a)' *.go
gofmt -w -r 'resp.Write([]byte(a + "\n")) -> fmt.Fprintln(resp, a)' *.go
gofmt -w -r 'resp.Write([]byte(a)) -> fmt.Fprint(resp, a)' *.go
8 years ago
"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 ( ) ,
}
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
sum . Nodes = append ( sum . Nodes , csn . Node . Node )
sum . Kind = svc . Kind
sum . Datacenter = csn . Node . Datacenter
sum . InstanceCount += 1
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
}