github.com/cilium/cilium@v1.16.2/pkg/ciliumenvoyconfig/envoy_l7lb_backend_syncer.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package ciliumenvoyconfig 5 6 import ( 7 "context" 8 "fmt" 9 "strconv" 10 11 envoy_config_core "github.com/cilium/proxy/go/envoy/config/core/v3" 12 envoy_config_endpoint "github.com/cilium/proxy/go/envoy/config/endpoint/v3" 13 "github.com/sirupsen/logrus" 14 15 "github.com/cilium/cilium/pkg/envoy" 16 "github.com/cilium/cilium/pkg/loadbalancer" 17 "github.com/cilium/cilium/pkg/lock" 18 "github.com/cilium/cilium/pkg/logging/logfields" 19 "github.com/cilium/cilium/pkg/service" 20 "github.com/cilium/cilium/pkg/slices" 21 ) 22 23 const anyPort = "*" 24 25 // envoyServiceBackendSyncer syncs the backends of a Service as Endpoints to the Envoy L7 proxy. 26 type envoyServiceBackendSyncer struct { 27 logger logrus.FieldLogger 28 29 envoyXdsServer envoy.XDSServer 30 31 l7lbSvcsMutex lock.RWMutex 32 l7lbSvcs map[loadbalancer.ServiceName]*backendSyncInfo 33 } 34 35 var _ service.BackendSyncer = &envoyServiceBackendSyncer{} 36 37 func (*envoyServiceBackendSyncer) ProxyName() string { 38 return "Envoy" 39 } 40 41 func newEnvoyServiceBackendSyncer(logger logrus.FieldLogger, envoyXdsServer envoy.XDSServer) *envoyServiceBackendSyncer { 42 return &envoyServiceBackendSyncer{ 43 logger: logger, 44 envoyXdsServer: envoyXdsServer, 45 l7lbSvcs: map[loadbalancer.ServiceName]*backendSyncInfo{}, 46 } 47 } 48 49 func (r *envoyServiceBackendSyncer) Sync(svc *loadbalancer.SVC) error { 50 r.l7lbSvcsMutex.RLock() 51 l7lbInfo, exists := r.l7lbSvcs[svc.Name] 52 if !exists { 53 r.l7lbSvcsMutex.RUnlock() 54 return nil 55 } 56 frontendPorts := l7lbInfo.GetAllFrontendPorts() 57 r.l7lbSvcsMutex.RUnlock() 58 59 // Filter backend based on list of port numbers, then upsert backends 60 // as Envoy endpoints 61 be := filterServiceBackends(svc, frontendPorts) 62 63 r.logger. 64 WithField("filteredBackends", be). 65 WithField(logfields.L7LBFrontendPorts, frontendPorts). 66 WithField(logfields.ServiceNamespace, svc.Name.Namespace). 67 WithField(logfields.ServiceName, svc.Name.Name). 68 Debug("Upsert envoy endpoints") 69 if err := r.upsertEnvoyEndpoints(svc.Name, be); err != nil { 70 return fmt.Errorf("failed to update backends in Envoy: %w", err) 71 } 72 73 return nil 74 } 75 76 func (r *envoyServiceBackendSyncer) RegisterServiceUsageInCEC(svcName loadbalancer.ServiceName, resourceName service.L7LBResourceName, frontendPorts []string) { 77 r.l7lbSvcsMutex.Lock() 78 defer r.l7lbSvcsMutex.Unlock() 79 80 l7lbInfo, exists := r.l7lbSvcs[svcName] 81 82 if !exists { 83 l7lbInfo = &backendSyncInfo{} 84 } 85 86 if l7lbInfo.backendRefs == nil { 87 l7lbInfo.backendRefs = make(map[service.L7LBResourceName]backendSyncCECInfo, 1) 88 } 89 90 l7lbInfo.backendRefs[resourceName] = backendSyncCECInfo{ 91 frontendPorts: frontendPorts, 92 } 93 94 r.l7lbSvcs[svcName] = l7lbInfo 95 } 96 97 func (r *envoyServiceBackendSyncer) DeregisterServiceUsageInCEC(svcName loadbalancer.ServiceName, resourceName service.L7LBResourceName) bool { 98 r.l7lbSvcsMutex.Lock() 99 defer r.l7lbSvcsMutex.Unlock() 100 101 l7lbInfo, exists := r.l7lbSvcs[svcName] 102 103 if !exists { 104 return false 105 } 106 107 if l7lbInfo.backendRefs != nil { 108 delete(l7lbInfo.backendRefs, resourceName) 109 } 110 111 // Cleanup service if it's no longer used by any CEC 112 if len(l7lbInfo.backendRefs) == 0 { 113 delete(r.l7lbSvcs, svcName) 114 return true 115 } 116 117 r.l7lbSvcs[svcName] = l7lbInfo 118 119 return false 120 } 121 122 func (r *envoyServiceBackendSyncer) upsertEnvoyEndpoints(serviceName loadbalancer.ServiceName, backendMap map[string][]*loadbalancer.Backend) error { 123 var resources envoy.Resources 124 125 resources.Endpoints = getEndpointsForLBBackends(serviceName, backendMap) 126 127 // Using context.TODO() is fine as we do not upsert listener resources here - the 128 // context ends up being used only if listener(s) are included in 'resources'. 129 return r.envoyXdsServer.UpsertEnvoyResources(context.TODO(), resources) 130 } 131 132 func getEndpointsForLBBackends(serviceName loadbalancer.ServiceName, backendMap map[string][]*loadbalancer.Backend) []*envoy_config_endpoint.ClusterLoadAssignment { 133 var endpoints []*envoy_config_endpoint.ClusterLoadAssignment 134 135 for port, bes := range backendMap { 136 var lbEndpoints []*envoy_config_endpoint.LbEndpoint 137 for _, be := range bes { 138 if be.Protocol != loadbalancer.TCP { 139 // Only TCP services supported with Envoy for now 140 continue 141 } 142 143 lbEndpoints = append(lbEndpoints, &envoy_config_endpoint.LbEndpoint{ 144 HostIdentifier: &envoy_config_endpoint.LbEndpoint_Endpoint{ 145 Endpoint: &envoy_config_endpoint.Endpoint{ 146 Address: &envoy_config_core.Address{ 147 Address: &envoy_config_core.Address_SocketAddress{ 148 SocketAddress: &envoy_config_core.SocketAddress{ 149 Address: be.AddrCluster.String(), 150 PortSpecifier: &envoy_config_core.SocketAddress_PortValue{ 151 PortValue: uint32(be.Port), 152 }, 153 }, 154 }, 155 }, 156 }, 157 }, 158 }) 159 } 160 161 endpoint := &envoy_config_endpoint.ClusterLoadAssignment{ 162 ClusterName: fmt.Sprintf("%s:%s", serviceName.String(), port), 163 Endpoints: []*envoy_config_endpoint.LocalityLbEndpoints{ 164 { 165 LbEndpoints: lbEndpoints, 166 }, 167 }, 168 } 169 endpoints = append(endpoints, endpoint) 170 171 // for backward compatibility, if any port is allowed, publish one more 172 // endpoint having cluster name as service name. 173 if port == anyPort { 174 endpoints = append(endpoints, &envoy_config_endpoint.ClusterLoadAssignment{ 175 ClusterName: serviceName.String(), 176 Endpoints: []*envoy_config_endpoint.LocalityLbEndpoints{ 177 { 178 LbEndpoints: lbEndpoints, 179 }, 180 }, 181 }) 182 } 183 } 184 185 return endpoints 186 } 187 188 // filterServiceBackends returns the list of backends based on given front end ports. 189 // The returned map will have key as port name/number, and value as list of respective backends. 190 func filterServiceBackends(svc *loadbalancer.SVC, onlyPorts []string) map[string][]*loadbalancer.Backend { 191 preferredBackends := filterPreferredBackends(svc.Backends) 192 193 if len(onlyPorts) == 0 { 194 return map[string][]*loadbalancer.Backend{ 195 "*": preferredBackends, 196 } 197 } 198 199 res := map[string][]*loadbalancer.Backend{} 200 for _, port := range onlyPorts { 201 // check for port number 202 if port == strconv.Itoa(int(svc.Frontend.Port)) { 203 res[port] = preferredBackends 204 } 205 206 // Continue checking for either named port as the same service 207 // can be used with multiple port types together 208 for _, backend := range preferredBackends { 209 if port == backend.FEPortName { 210 res[port] = append(res[port], backend) 211 } 212 } 213 } 214 215 return res 216 } 217 218 // filterPreferredBackends returns the slice of backends which are preferred for the given service. 219 // If there is no preferred backend, it returns the slice of all backends. 220 func filterPreferredBackends(backends []*loadbalancer.Backend) []*loadbalancer.Backend { 221 var res []*loadbalancer.Backend 222 for _, backend := range backends { 223 if backend.Preferred { 224 res = append(res, backend) 225 } 226 } 227 if len(res) > 0 { 228 return res 229 } 230 231 return backends 232 } 233 234 type backendSyncInfo struct { 235 // Names of the L7 LB resources (e.g. CEC) that need this service's backends to be 236 // synced to an L7 Loadbalancer. 237 backendRefs map[service.L7LBResourceName]backendSyncCECInfo 238 } 239 240 func (r *backendSyncInfo) GetAllFrontendPorts() []string { 241 allPorts := []string{} 242 243 for _, info := range r.backendRefs { 244 allPorts = append(allPorts, info.frontendPorts...) 245 } 246 247 return slices.SortedUnique(allPorts) 248 } 249 250 type backendSyncCECInfo struct { 251 // List of front-end ports of upstream service/cluster, which will be used for 252 // filtering applicable endpoints. 253 // 254 // If nil, all the available backends will be used. 255 frontendPorts []string 256 }