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  }