// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package proxycfg
import (
"context"
"fmt"
"net"
"sort"
"strconv"
"strings"
"time"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/consul/acl"
cachetype "github.com/hashicorp/consul/agent/cache-types"
"github.com/hashicorp/consul/agent/leafcert"
"github.com/hashicorp/consul/agent/proxycfg/internal/watch"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib/maps"
"github.com/hashicorp/consul/logging"
"github.com/hashicorp/consul/proto/private/pbpeering"
)
type handlerMeshGateway struct {
handlerState
}
type peerAddressType string
const (
undefinedAddressType peerAddressType = ""
ipAddressType peerAddressType = "ip"
hostnameAddressType peerAddressType = "hostname"
)
// initialize sets up the watches needed based on the current mesh gateway registration
func ( s * handlerMeshGateway ) initialize ( ctx context . Context ) ( ConfigSnapshot , error ) {
snap := newConfigSnapshotFromServiceInstance ( s . serviceInstance , s . stateConfig )
snap . MeshGateway . WatchedLocalServers = watch . NewMap [ string , structs . CheckServiceNodes ] ( )
// Watch for root changes
err := s . dataSources . CARoots . Notify ( ctx , & structs . DCSpecificRequest {
Datacenter : s . source . Datacenter ,
QueryOptions : structs . QueryOptions { Token : s . token } ,
Source : * s . source ,
} , rootsWatchID , s . ch )
if err != nil {
return snap , err
}
// Watch for all peer trust bundles we may need.
err = s . dataSources . TrustBundleList . Notify ( ctx , & cachetype . TrustBundleListRequest {
Request : & pbpeering . TrustBundleListByServiceRequest {
Kind : string ( structs . ServiceKindMeshGateway ) ,
ServiceName : s . service ,
Namespace : s . proxyID . NamespaceOrDefault ( ) ,
Partition : s . proxyID . PartitionOrDefault ( ) ,
} ,
QueryOptions : structs . QueryOptions { Token : s . token } ,
} , peeringTrustBundlesWatchID , s . ch )
if err != nil {
return snap , err
}
wildcardEntMeta := s . proxyID . WithWildcardNamespace ( )
// Watch for all services.
// Eventually we will have to watch connect enabled instances for each service as well as the
// destination services themselves but those notifications will be setup later.
// We cannot setup those watches until we know what the services are.
err = s . dataSources . ServiceList . Notify ( ctx , & structs . DCSpecificRequest {
Datacenter : s . source . Datacenter ,
QueryOptions : structs . QueryOptions { Token : s . token } ,
Source : * s . source ,
EnterpriseMeta : * wildcardEntMeta ,
} , serviceListWatchID , s . ch )
if err != nil {
return snap , err
}
// Watch service-resolvers so we can setup service subset clusters
err = s . dataSources . ConfigEntryList . Notify ( ctx , & structs . ConfigEntryQuery {
Datacenter : s . source . Datacenter ,
QueryOptions : structs . QueryOptions { Token : s . token } ,
Kind : structs . ServiceResolver ,
EnterpriseMeta : * wildcardEntMeta ,
} , serviceResolversWatchID , s . ch )
if err != nil {
s . logger . Named ( logging . MeshGateway ) .
Error ( "failed to register watch for service-resolver config entries" , "error" , err )
return snap , err
}
if s . proxyID . InDefaultPartition ( ) {
if err := s . initializeCrossDCWatches ( ctx , & snap ) ; err != nil {
return snap , err
}
}
if err := s . initializeEntWatches ( ctx ) ; err != nil {
return snap , err
}
// Get information about the entire service mesh.
err = s . dataSources . ConfigEntry . Notify ( ctx , & structs . ConfigEntryQuery {
Kind : structs . MeshConfig ,
Name : structs . MeshConfigMesh ,
Datacenter : s . source . Datacenter ,
QueryOptions : structs . QueryOptions { Token : s . token } ,
EnterpriseMeta : * structs . DefaultEnterpriseMetaInPartition ( s . proxyID . PartitionOrDefault ( ) ) ,
} , meshConfigEntryID , s . ch )
if err != nil {
return snap , err
}
// Watch for all exported services from this mesh gateway's partition in any peering.
err = s . dataSources . ExportedPeeredServices . Notify ( ctx , & structs . DCSpecificRequest {
Datacenter : s . source . Datacenter ,
QueryOptions : structs . QueryOptions { Token : s . token } ,
Source : * s . source ,
EnterpriseMeta : s . proxyID . EnterpriseMeta ,
} , exportedServiceListWatchID , s . ch )
if err != nil {
return snap , err
}
// Watch for service default object that matches this mesh gateway's name
err = s . dataSources . ConfigEntry . Notify ( ctx , & structs . ConfigEntryQuery {
Kind : structs . ServiceDefaults ,
Name : s . service ,
Datacenter : s . source . Datacenter ,
QueryOptions : structs . QueryOptions { Token : s . token } ,
EnterpriseMeta : s . proxyID . EnterpriseMeta ,
} , serviceDefaultsWatchID , s . ch )
if err != nil {
return snap , err
}
snap . MeshGateway . WatchedServices = make ( map [ structs . ServiceName ] context . CancelFunc )
snap . MeshGateway . WatchedGateways = make ( map [ string ] context . CancelFunc )
snap . MeshGateway . ServiceGroups = make ( map [ structs . ServiceName ] structs . CheckServiceNodes )
snap . MeshGateway . GatewayGroups = make ( map [ string ] structs . CheckServiceNodes )
snap . MeshGateway . ServiceResolvers = make ( map [ structs . ServiceName ] * structs . ServiceResolverConfigEntry )
snap . MeshGateway . HostnameDatacenters = make ( map [ string ] structs . CheckServiceNodes )
snap . MeshGateway . ExportedServicesWithPeers = make ( map [ structs . ServiceName ] [ ] string )
snap . MeshGateway . DiscoveryChain = make ( map [ structs . ServiceName ] * structs . CompiledDiscoveryChain )
snap . MeshGateway . WatchedDiscoveryChains = make ( map [ structs . ServiceName ] context . CancelFunc )
snap . MeshGateway . WatchedPeeringServices = make ( map [ string ] map [ structs . ServiceName ] context . CancelFunc )
snap . MeshGateway . WatchedPeers = make ( map [ string ] context . CancelFunc )
snap . MeshGateway . PeeringServices = make ( map [ string ] map [ structs . ServiceName ] PeeringServiceValue )
// there is no need to initialize the map of service resolvers as we
// fully rebuild it every time we get updates
return snap , err
}
func ( s * handlerMeshGateway ) initializeCrossDCWatches ( ctx context . Context , snap * ConfigSnapshot ) error {
if s . meta [ structs . MetaWANFederationKey ] == "1" {
// Conveniently we can just use this service meta attribute in one
// place here to set the machinery in motion and leave the conditional
// behavior out of the rest of the package.
err := s . dataSources . FederationStateListMeshGateways . Notify ( ctx , & structs . DCSpecificRequest {
Datacenter : s . source . Datacenter ,
QueryOptions : structs . QueryOptions { Token : s . token } ,
Source : * s . source ,
} , federationStateListGatewaysWatchID , s . ch )
if err != nil {
return err
}
err = s . dataSources . Health . Notify ( ctx , & structs . ServiceSpecificRequest {
Datacenter : s . source . Datacenter ,
QueryOptions : structs . QueryOptions { Token : s . token } ,
ServiceName : structs . ConsulServiceName ,
} , consulServerListWatchID , s . ch )
if err != nil {
return err
}
snap . MeshGateway . WatchedLocalServers . InitWatch ( structs . ConsulServiceName , nil )
}
err := s . dataSources . Datacenters . Notify ( ctx , & structs . DatacentersRequest {
QueryOptions : structs . QueryOptions { Token : s . token , MaxAge : 30 * time . Second } ,
} , datacentersWatchID , s . ch )
if err != nil {
return err
}
// Once we start getting notified about the datacenters we will setup watches on the
// gateways within those other datacenters. We cannot do that here because we don't
// know what they are yet.
return nil
}
func ( s * handlerMeshGateway ) handleUpdate ( ctx context . Context , u UpdateEvent , snap * ConfigSnapshot ) error {
if u . Err != nil {
return fmt . Errorf ( "error filling agent cache: %v" , u . Err )
}
meshLogger := s . logger . Named ( logging . MeshGateway )
switch u . CorrelationID {
case rootsWatchID :
roots , ok := u . Result . ( * structs . IndexedCARoots )
if ! ok {
return fmt . Errorf ( "invalid type for response: %T" , u . Result )
}
snap . Roots = roots
case federationStateListGatewaysWatchID :
dcIndexedNodes , ok := u . Result . ( * structs . DatacenterIndexedCheckServiceNodes )
if ! ok {
return fmt . Errorf ( "invalid type for response: %T" , u . Result )
}
snap . MeshGateway . FedStateGateways = dcIndexedNodes . DatacenterNodes
for dc , nodes := range dcIndexedNodes . DatacenterNodes {
snap . MeshGateway . HostnameDatacenters [ dc ] = hostnameEndpoints (
s . logger . Named ( logging . MeshGateway ) ,
snap . Locality ,
nodes ,
)
}
for dc := range snap . MeshGateway . HostnameDatacenters {
if _ , ok := dcIndexedNodes . DatacenterNodes [ dc ] ; ! ok {
delete ( snap . MeshGateway . HostnameDatacenters , dc )
}
}
case serviceListWatchID :
services , ok := u . Result . ( * structs . IndexedServiceList )
if ! ok {
return fmt . Errorf ( "invalid type for response: %T" , u . Result )
}
svcMap := make ( map [ structs . ServiceName ] struct { } )
for _ , svc := range services . Services {
// Make sure to add every service to this map, we use it to cancel
// watches below.
svcMap [ svc ] = struct { } { }
if _ , ok := snap . MeshGateway . WatchedServices [ svc ] ; ! ok {
ctx , cancel := context . WithCancel ( ctx )
err := s . dataSources . Health . Notify ( ctx , & structs . ServiceSpecificRequest {
Datacenter : s . source . Datacenter ,
QueryOptions : structs . QueryOptions { Token : s . token } ,
ServiceName : svc . Name ,
Connect : true ,
EnterpriseMeta : svc . EnterpriseMeta ,
} , fmt . Sprintf ( "connect-service:%s" , svc . String ( ) ) , s . ch )
if err != nil {
meshLogger . Error ( "failed to register watch for connect-service" ,
"service" , svc . String ( ) ,
"error" , err ,
)
cancel ( )
return err
}
snap . MeshGateway . WatchedServices [ svc ] = cancel
}
}
for sid , cancelFn := range snap . MeshGateway . WatchedServices {
if _ , ok := svcMap [ sid ] ; ! ok {
meshLogger . Debug ( "canceling watch for service" , "service" , sid . String ( ) )
// TODO (gateways) Should the sid also be deleted from snap.MeshGateway.ServiceGroups?
// Do those endpoints get cleaned up some other way?
delete ( snap . MeshGateway . WatchedServices , sid )
cancelFn ( )
// always remove the sid from the ServiceGroups when un-watch the service
delete ( snap . MeshGateway . ServiceGroups , sid )
}
}
snap . MeshGateway . WatchedServicesSet = true
case datacentersWatchID :
datacentersRaw , ok := u . Result . ( * [ ] string )
if ! ok {
return fmt . Errorf ( "invalid type for response: %T" , u . Result )
}
if datacentersRaw == nil {
return fmt . Errorf ( "invalid response with a nil datacenter list" )
}
datacenters := * datacentersRaw
for _ , dc := range datacenters {
if dc == s . source . Datacenter {
continue
}
entMeta := structs . DefaultEnterpriseMetaInDefaultPartition ( )
gk := GatewayKey { Datacenter : dc , Partition : entMeta . PartitionOrDefault ( ) }
if _ , ok := snap . MeshGateway . WatchedGateways [ gk . String ( ) ] ; ! ok {
ctx , cancel := context . WithCancel ( ctx )
err := s . dataSources . InternalServiceDump . Notify ( ctx , & structs . ServiceDumpRequest {
Datacenter : dc ,
QueryOptions : structs . QueryOptions { Token : s . token } ,
ServiceKind : structs . ServiceKindMeshGateway ,
UseServiceKind : true ,
NodesOnly : true ,
Source : * s . source ,
EnterpriseMeta : * entMeta ,
} , fmt . Sprintf ( "mesh-gateway:%s" , gk . String ( ) ) , s . ch )
if err != nil {
meshLogger . Error ( "failed to register watch for mesh-gateway" ,
"datacenter" , dc ,
"partition" , entMeta . PartitionOrDefault ( ) ,
"error" , err ,
)
cancel ( )
return err
}
snap . MeshGateway . WatchedGateways [ gk . String ( ) ] = cancel
}
}
for key , cancelFn := range snap . MeshGateway . WatchedGateways {
gk := gatewayKeyFromString ( key )
if gk . Datacenter == s . source . Datacenter {
// Only cross-DC watches are managed by the datacenters watch.
continue
}
found := false
for _ , dcCurrent := range datacenters {
if dcCurrent == gk . Datacenter {
found = true
break
}
}
if ! found {
delete ( snap . MeshGateway . WatchedGateways , key )
cancelFn ( )
}
}
case serviceResolversWatchID :
configEntries , ok := u . Result . ( * structs . IndexedConfigEntries )
if ! ok {
return fmt . Errorf ( "invalid type for response: %T" , u . Result )
}
resolvers := make ( map [ structs . ServiceName ] * structs . ServiceResolverConfigEntry )
for _ , entry := range configEntries . Entries {
if resolver , ok := entry . ( * structs . ServiceResolverConfigEntry ) ; ok {
resolvers [ structs . NewServiceName ( resolver . Name , & resolver . EnterpriseMeta ) ] = resolver
}
}
snap . MeshGateway . ServiceResolvers = resolvers
case consulServerListWatchID :
resp , ok := u . Result . ( * structs . IndexedCheckServiceNodes )
if ! ok {
return fmt . Errorf ( "invalid type for response: %T" , u . Result )
}
for _ , csn := range resp . Nodes {
if csn . Service . Service != structs . ConsulServiceName {
return fmt . Errorf ( "expected service name %q but got %q" ,
structs . ConsulServiceName , csn . Service . Service )
}
if csn . Node . Datacenter != snap . Datacenter {
return fmt . Errorf ( "expected datacenter %q but got %q" ,
snap . Datacenter , csn . Node . Datacenter )
}
}
snap . MeshGateway . WatchedLocalServers . Set ( structs . ConsulServiceName , resp . Nodes )
case exportedServiceListWatchID :
exportedServices , ok := u . Result . ( * structs . IndexedExportedServiceList )
if ! ok {
return fmt . Errorf ( "invalid type for response: %T" , u . Result )
}
seenServices := make ( map [ structs . ServiceName ] [ ] string ) // svc -> peername slice
for peerName , services := range exportedServices . Services {
for _ , svc := range services {
seenServices [ svc ] = append ( seenServices [ svc ] , peerName )
}
}
// Sort the peer names so ultimately xDS has a stable output.
for svc := range seenServices {
sort . Strings ( seenServices [ svc ] )
}
peeredServiceList := maps . SliceOfKeys ( seenServices )
structs . ServiceList ( peeredServiceList ) . Sort ( )
snap . MeshGateway . ExportedServicesSlice = peeredServiceList
snap . MeshGateway . ExportedServicesWithPeers = seenServices
snap . MeshGateway . ExportedServicesSet = true
// Decide if we do or do not need our leaf.
hasExports := len ( snap . MeshGateway . ExportedServicesSlice ) > 0
if hasExports && snap . MeshGateway . LeafCertWatchCancel == nil {
// no watch and we need one
ctx , cancel := context . WithCancel ( ctx )
err := s . dataSources . LeafCertificate . Notify ( ctx , & leafcert . ConnectCALeafRequest {
Datacenter : s . source . Datacenter ,
Token : s . token ,
Kind : structs . ServiceKindMeshGateway ,
EnterpriseMeta : s . proxyID . EnterpriseMeta ,
} , leafWatchID , s . ch )
if err != nil {
cancel ( )
return err
}
snap . MeshGateway . LeafCertWatchCancel = cancel
} else if ! hasExports && snap . MeshGateway . LeafCertWatchCancel != nil {
// has watch and shouldn't
snap . MeshGateway . LeafCertWatchCancel ( )
snap . MeshGateway . LeafCertWatchCancel = nil
snap . MeshGateway . Leaf = nil
}
// For each service that we should be exposing, also watch disco chains
// in the same manner as an ingress gateway would.
for _ , svc := range snap . MeshGateway . ExportedServicesSlice {
if _ , ok := snap . MeshGateway . WatchedDiscoveryChains [ svc ] ; ok {
continue
}
ctx , cancel := context . WithCancel ( ctx )
err := s . dataSources . CompiledDiscoveryChain . Notify ( ctx , & structs . DiscoveryChainRequest {
Datacenter : s . source . Datacenter ,
QueryOptions : structs . QueryOptions { Token : s . token } ,
Name : svc . Name ,
EvaluateInDatacenter : s . source . Datacenter ,
EvaluateInNamespace : svc . NamespaceOrDefault ( ) ,
EvaluateInPartition : svc . PartitionOrDefault ( ) ,
} , "discovery-chain:" + svc . String ( ) , s . ch )
if err != nil {
meshLogger . Error ( "failed to register watch for discovery chain" ,
"service" , svc . String ( ) ,
"error" , err ,
)
cancel ( )
return err
}
snap . MeshGateway . WatchedDiscoveryChains [ svc ] = cancel
}
// Clean up data from services that were not in the update
for svc , cancelFn := range snap . MeshGateway . WatchedDiscoveryChains {
if _ , ok := seenServices [ svc ] ; ! ok {
cancelFn ( )
delete ( snap . MeshGateway . WatchedDiscoveryChains , svc )
}
}
// These entries are intentionally handled separately from the
// WatchedDiscoveryChains above. There have been situations where a
// discovery watch was cancelled, then fired. That update event then
// re-populated the DiscoveryChain map entry, which wouldn't get
// cleaned up since there was no known watch for it.
for svc := range snap . MeshGateway . DiscoveryChain {
if _ , ok := seenServices [ svc ] ; ! ok {
delete ( snap . MeshGateway . DiscoveryChain , svc )
}
}
case leafWatchID :
leaf , ok := u . Result . ( * structs . IssuedCert )
if ! ok {
return fmt . Errorf ( "invalid type for response: %T" , u . Result )
}
if hasExports := len ( snap . MeshGateway . ExportedServicesSlice ) > 0 ; ! hasExports {
return nil // ignore this update, it's stale
}
snap . MeshGateway . Leaf = leaf
case peeringTrustBundlesWatchID :
resp , ok := u . Result . ( * pbpeering . TrustBundleListByServiceResponse )
if ! ok {
return fmt . Errorf ( "invalid type for response: %T" , u . Result )
}
if len ( resp . Bundles ) > 0 {
snap . MeshGateway . PeeringTrustBundles = resp . Bundles
}
snap . MeshGateway . PeeringTrustBundlesSet = true
wildcardEntMeta := s . proxyID . WithWildcardNamespace ( )
// For each peer, fetch the imported services to support mesh gateway local
// mode.
for _ , tb := range resp . Bundles {
entMeta := structs . DefaultEnterpriseMetaInDefaultPartition ( )
if _ , ok := snap . MeshGateway . WatchedPeers [ tb . PeerName ] ; ! ok {
ctx , cancel := context . WithCancel ( ctx )
err := s . dataSources . ServiceList . Notify ( ctx , & structs . DCSpecificRequest {
PeerName : tb . PeerName ,
QueryOptions : structs . QueryOptions { Token : s . token } ,
Source : * s . source ,
EnterpriseMeta : * wildcardEntMeta ,
} , peeringServiceListWatchID + tb . PeerName , s . ch )
if err != nil {
meshLogger . Error ( "failed to register watch for mesh-gateway" ,
"peer" , tb . PeerName ,
"partition" , entMeta . PartitionOrDefault ( ) ,
"error" , err ,
)
cancel ( )
return err
}
snap . MeshGateway . WatchedPeers [ tb . PeerName ] = cancel
}
}
for peerName , cancelFn := range snap . MeshGateway . WatchedPeers {
found := false
for _ , bundle := range resp . Bundles {
if peerName == bundle . PeerName {
found = true
break
}
}
if ! found {
delete ( snap . MeshGateway . PeeringServices , peerName )
delete ( snap . MeshGateway . WatchedPeers , peerName )
delete ( snap . MeshGateway . WatchedPeeringServices , peerName )
cancelFn ( )
}
}
case meshConfigEntryID :
resp , ok := u . Result . ( * structs . ConfigEntryResponse )
if ! ok {
return fmt . Errorf ( "invalid type for response: %T" , u . Result )
}
meshConf , ok := resp . Entry . ( * structs . MeshConfigEntry )
if resp . Entry != nil && ! ok {
return fmt . Errorf ( "invalid type for config entry: %T" , resp . Entry )
}
snap . MeshGateway . MeshConfig = meshConf
snap . MeshGateway . MeshConfigSet = true
// If we're peering through mesh gateways it means the config entry may be deleted
// or the flag was disabled. Here we clean up related watches if they exist.
if ! meshConf . PeerThroughMeshGateways ( ) {
// We avoid canceling server watches when WAN federation is enabled since it
// always requires a watch to the local servers.
if s . meta [ structs . MetaWANFederationKey ] != "1" {
// If the entry was deleted we cancel watches that may have existed because of
// PeerThroughMeshGateways being set in the past.
snap . MeshGateway . WatchedLocalServers . CancelWatch ( structs . ConsulServiceName )
}
if snap . MeshGateway . PeerServersWatchCancel != nil {
snap . MeshGateway . PeerServersWatchCancel ( )
snap . MeshGateway . PeerServersWatchCancel = nil
snap . MeshGateway . PeerServers = nil
}
return nil
}
// If PeerThroughMeshGateways is enabled, and we are in the default partition,
// we need to start watching the list of peering connections in all partitions
// to set up outbound routes for the control plane. Consul servers are in the default partition,
// so only mesh gateways here have his responsibility.
if snap . ProxyID . InDefaultPartition ( ) &&
snap . MeshGateway . PeerServersWatchCancel == nil {
peeringListCtx , cancel := context . WithCancel ( ctx )
err := s . dataSources . PeeringList . Notify ( peeringListCtx , & cachetype . PeeringListRequest {
Request : & pbpeering . PeeringListRequest {
Partition : acl . WildcardPartitionName ,
} ,
QueryOptions : structs . QueryOptions { Token : s . token } ,
} , peerServersWatchID , s . ch )
if err != nil {
meshLogger . Error ( "failed to register watch for peering list" , "error" , err )
cancel ( )
return err
}
snap . MeshGateway . PeerServersWatchCancel = cancel
}
// We avoid initializing Consul server watches when WAN federation is enabled since it
// always requires server watches.
if s . meta [ structs . MetaWANFederationKey ] == "1" {
return nil
}
if snap . MeshGateway . WatchedLocalServers . IsWatched ( structs . ConsulServiceName ) {
return nil
}
notifyCtx , cancel := context . WithCancel ( ctx )
err := s . dataSources . Health . Notify ( notifyCtx , & structs . ServiceSpecificRequest {
Datacenter : s . source . Datacenter ,
QueryOptions : structs . QueryOptions { Token : s . token } ,
ServiceName : structs . ConsulServiceName ,
} , consulServerListWatchID , s . ch )
if err != nil {
cancel ( )
return fmt . Errorf ( "failed to watch local consul servers: %w" , err )
}
snap . MeshGateway . WatchedLocalServers . InitWatch ( structs . ConsulServiceName , cancel )
case peerServersWatchID :
resp , ok := u . Result . ( * pbpeering . PeeringListResponse )
if ! ok {
return fmt . Errorf ( "invalid type for response: %T" , u . Result )
}
peerServers := make ( map [ string ] PeerServersValue )
for _ , peering := range resp . Peerings {
// We only need to keep track of outbound establish connections for mesh gateway.
// We could also check for the peering status, but this requires a response from the leader
// which holds the peerstream information. We want to allow stale reads so there could be peerings in
// a deleting or terminating state.
if ! peering . ShouldDial ( ) {
continue
}
if existing , ok := peerServers [ peering . PeerServerName ] ; ok && existing . Index >= peering . ModifyIndex {
// Multiple peerings can reference the same set of Consul servers, since there can be
// multiple partitions in a datacenter. Rather than randomly overwriting, we attempt to
// use the latest addresses by checking the Raft index associated with the peering.
continue
}
hostnames , ips := peerHostnamesAndIPs ( meshLogger , peering . Name , peering . GetAddressesToDial ( ) )
if len ( hostnames ) > 0 {
peerServers [ peering . PeerServerName ] = PeerServersValue {
Addresses : hostnames ,
Index : peering . ModifyIndex ,
UseCDS : true ,
}
} else if len ( ips ) > 0 {
peerServers [ peering . PeerServerName ] = PeerServersValue {
Addresses : ips ,
Index : peering . ModifyIndex ,
}
}
}
snap . MeshGateway . PeerServers = peerServers
case serviceDefaultsWatchID :
resp , ok := u . Result . ( * structs . ConfigEntryResponse )
if ! ok {
return fmt . Errorf ( "invalid type for config entry: %T" , resp . Entry )
}
if resp . Entry == nil {
return nil
}
serviceDefaults , ok := resp . Entry . ( * structs . ServiceConfigEntry )
if ! ok {
return fmt . Errorf ( "invalid type for config entry: %T" , resp . Entry )
}
if serviceDefaults . UpstreamConfig != nil && serviceDefaults . UpstreamConfig . Defaults != nil {
if serviceDefaults . UpstreamConfig . Defaults . Limits != nil {
snap . MeshGateway . Limits = serviceDefaults . UpstreamConfig . Defaults . Limits
}
}
default :
switch {
case strings . HasPrefix ( u . CorrelationID , peeringServiceListWatchID ) :
services , ok := u . Result . ( * structs . IndexedServiceList )
if ! ok {
return fmt . Errorf ( "invalid type for response: %T" , u . Result )
}
peerName := strings . TrimPrefix ( u . CorrelationID , peeringServiceListWatchID )
svcMap := make ( map [ structs . ServiceName ] struct { } )
if _ , ok := snap . MeshGateway . WatchedPeeringServices [ peerName ] ; ! ok {
snap . MeshGateway . WatchedPeeringServices [ peerName ] = make ( map [ structs . ServiceName ] context . CancelFunc )
}
for _ , svc := range services . Services {
// Make sure to add every service to this map, we use it to cancel
// watches below.
svcMap [ svc ] = struct { } { }
if _ , ok := snap . MeshGateway . WatchedPeeringServices [ peerName ] [ svc ] ; ! ok {
ctx , cancel := context . WithCancel ( ctx )
err := s . dataSources . Health . Notify ( ctx , & structs . ServiceSpecificRequest {
PeerName : peerName ,
QueryOptions : structs . QueryOptions { Token : s . token } ,
ServiceName : svc . Name ,
Connect : true ,
EnterpriseMeta : svc . EnterpriseMeta ,
} , fmt . Sprintf ( "peering-connect-service:%s:%s" , peerName , svc . String ( ) ) , s . ch )
if err != nil {
meshLogger . Error ( "failed to register watch for connect-service" ,
"service" , svc . String ( ) ,
"error" , err ,
)
cancel ( )
return err
}
snap . MeshGateway . WatchedPeeringServices [ peerName ] [ svc ] = cancel
}
}
watchedServices := snap . MeshGateway . WatchedPeeringServices [ peerName ]
for sn , cancelFn := range watchedServices {
if _ , ok := svcMap [ sn ] ; ! ok {
meshLogger . Debug ( "canceling watch for service" , "service" , sn . String ( ) )
delete ( snap . MeshGateway . WatchedPeeringServices [ peerName ] , sn )
delete ( snap . MeshGateway . PeeringServices [ peerName ] , sn )
cancelFn ( )
}
}
case strings . HasPrefix ( u . CorrelationID , "connect-service:" ) :
resp , ok := u . Result . ( * structs . IndexedCheckServiceNodes )
if ! ok {
return fmt . Errorf ( "invalid type for response: %T" , u . Result )
}
sn := structs . ServiceNameFromString ( strings . TrimPrefix ( u . CorrelationID , "connect-service:" ) )
if len ( resp . Nodes ) > 0 {
snap . MeshGateway . ServiceGroups [ sn ] = resp . Nodes
} else if _ , ok := snap . MeshGateway . ServiceGroups [ sn ] ; ok {
delete ( snap . MeshGateway . ServiceGroups , sn )
}
case strings . HasPrefix ( u . CorrelationID , "peering-connect-service:" ) :
resp , ok := u . Result . ( * structs . IndexedCheckServiceNodes )
if ! ok {
return fmt . Errorf ( "invalid type for response: %T" , u . Result )
}
key := strings . TrimPrefix ( u . CorrelationID , "peering-connect-service:" )
peer , snString , ok := strings . Cut ( key , ":" )
if ok {
sn := structs . ServiceNameFromString ( snString )
if len ( resp . Nodes ) > 0 {
if _ , ok := snap . MeshGateway . PeeringServices [ peer ] ; ! ok {
snap . MeshGateway . PeeringServices [ peer ] = make ( map [ structs . ServiceName ] PeeringServiceValue )
}
if eps := hostnameEndpoints ( s . logger , GatewayKey { } , resp . Nodes ) ; len ( eps ) > 0 {
snap . MeshGateway . PeeringServices [ peer ] [ sn ] = PeeringServiceValue {
Nodes : eps ,
UseCDS : true ,
}
} else {
snap . MeshGateway . PeeringServices [ peer ] [ sn ] = PeeringServiceValue {
Nodes : resp . Nodes ,
}
}
} else if _ , ok := snap . MeshGateway . PeeringServices [ peer ] ; ok {
delete ( snap . MeshGateway . PeeringServices [ peer ] , sn )
}
}
case strings . HasPrefix ( u . CorrelationID , "mesh-gateway:" ) :
resp , ok := u . Result . ( * structs . IndexedCheckServiceNodes )
if ! ok {
return fmt . Errorf ( "invalid type for response: %T" , u . Result )
}
key := strings . TrimPrefix ( u . CorrelationID , "mesh-gateway:" )
delete ( snap . MeshGateway . GatewayGroups , key )
delete ( snap . MeshGateway . HostnameDatacenters , key )
if len ( resp . Nodes ) > 0 {
snap . MeshGateway . GatewayGroups [ key ] = resp . Nodes
snap . MeshGateway . HostnameDatacenters [ key ] = hostnameEndpoints (
s . logger . Named ( logging . MeshGateway ) ,
snap . Locality ,
resp . Nodes ,
)
}
case strings . HasPrefix ( u . CorrelationID , "discovery-chain:" ) :
resp , ok := u . Result . ( * structs . DiscoveryChainResponse )
if ! ok {
return fmt . Errorf ( "invalid type for response: %T" , u . Result )
}
svcString := strings . TrimPrefix ( u . CorrelationID , "discovery-chain:" )
svc := structs . ServiceNameFromString ( svcString )
if ! snap . MeshGateway . IsServiceExported ( svc ) {
delete ( snap . MeshGateway . DiscoveryChain , svc )
s . logger . Trace ( "discovery-chain watch fired for unknown service" , "service" , svc )
return nil
}
snap . MeshGateway . DiscoveryChain [ svc ] = resp . Chain
default :
if err := s . handleEntUpdate ( meshLogger , ctx , u , snap ) ; err != nil {
return err
}
}
}
return nil
}
func peerHostnamesAndIPs ( logger hclog . Logger , peerName string , addresses [ ] string ) ( [ ] structs . ServiceAddress , [ ] structs . ServiceAddress ) {
var (
hostnames [ ] structs . ServiceAddress
ips [ ] structs . ServiceAddress
)
// Sort the input so that the output is also sorted.
sort . Strings ( addresses )
for _ , addr := range addresses {
ip , rawPort , splitErr := net . SplitHostPort ( addr )
port , convErr := strconv . Atoi ( rawPort )
if splitErr != nil || convErr != nil {
logger . Warn ( "unable to parse ip and port from peer server address. skipping address." ,
"peer" , peerName , "address" , addr )
}
if net . ParseIP ( ip ) != nil {
ips = append ( ips , structs . ServiceAddress {
Address : ip ,
Port : port ,
} )
} else {
hostnames = append ( hostnames , structs . ServiceAddress {
Address : ip ,
Port : port ,
} )
}
}
if len ( hostnames ) > 0 && len ( ips ) > 0 {
logger . Warn ( "peer server address list contains mix of hostnames and IP addresses; only hostnames will be passed to Envoy" ,
"peer" , peerName )
}
return hostnames , ips
}