github.com/cilium/cilium@v1.16.2/operator/pkg/lbipam/service_store.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package lbipam
     5  
     6  import (
     7  	"net"
     8  	"net/netip"
     9  	"slices"
    10  
    11  	"golang.org/x/exp/maps"
    12  
    13  	"github.com/cilium/cilium/pkg/k8s/resource"
    14  	slim_core_v1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/api/core/v1"
    15  	slim_labels "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/labels"
    16  )
    17  
    18  type serviceStore struct {
    19  	// List of services which have received all IPs they requested
    20  	satisfied map[resource.Key]*ServiceView
    21  	// List of services which have one or more IPs which were requested but not allocated
    22  	unsatisfied map[resource.Key]*ServiceView
    23  }
    24  
    25  func NewServiceStore() serviceStore {
    26  	return serviceStore{
    27  		satisfied:   make(map[resource.Key]*ServiceView),
    28  		unsatisfied: make(map[resource.Key]*ServiceView),
    29  	}
    30  }
    31  
    32  func (ss *serviceStore) GetService(key resource.Key) (serviceView *ServiceView, found, satisfied bool) {
    33  	serviceView, found = ss.satisfied[key]
    34  	if found {
    35  		return serviceView, true, true
    36  	}
    37  
    38  	serviceView, found = ss.unsatisfied[key]
    39  	if found {
    40  		return serviceView, true, false
    41  	}
    42  
    43  	return nil, false, false
    44  }
    45  
    46  func (ss *serviceStore) Upsert(serviceView *ServiceView) {
    47  	if serviceView.isSatisfied() {
    48  		delete(ss.unsatisfied, serviceView.Key)
    49  		ss.satisfied[serviceView.Key] = serviceView
    50  	} else {
    51  		delete(ss.satisfied, serviceView.Key)
    52  		ss.unsatisfied[serviceView.Key] = serviceView
    53  	}
    54  }
    55  
    56  func (ss *serviceStore) Delete(key resource.Key) {
    57  	delete(ss.satisfied, key)
    58  	delete(ss.unsatisfied, key)
    59  }
    60  
    61  // ServiceView is the LB IPAM's view of the service, the minimal amount of info we need about it.
    62  type ServiceView struct {
    63  	Key    resource.Key
    64  	Labels slim_labels.Set
    65  
    66  	Generation int64
    67  	Status     *slim_core_v1.ServiceStatus
    68  
    69  	SharingKey            string
    70  	SharingCrossNamespace []string
    71  	// These required to determine if a service conflicts with another for sharing an ip
    72  	ExternalTrafficPolicy slim_core_v1.ServiceExternalTrafficPolicy
    73  	Ports                 []slim_core_v1.ServicePort
    74  	Namespace             string
    75  	Selector              map[string]string
    76  
    77  	// The specific IPs requested by the service
    78  	RequestedIPs []netip.Addr
    79  	// The IP families requested by the service
    80  	RequestedFamilies struct {
    81  		IPv4 bool
    82  		IPv6 bool
    83  	}
    84  	// The IPs we have allocated for this IP
    85  	AllocatedIPs []ServiceViewIP
    86  }
    87  
    88  // isCompatible checks if two services are compatible for sharing an IP.
    89  func (sv *ServiceView) isCompatible(osv *ServiceView) bool {
    90  	// They have the same sharing key.
    91  	if sv.SharingKey != osv.SharingKey {
    92  		return false
    93  	}
    94  
    95  	// Services are namespaced, so services are only compatible if they are in the same namespace.
    96  	// This is for security reasons, otherwise a bad tennant could use a service in another namespace.
    97  	// We still allow cross-namespace sharing if specifically allowed on both services.
    98  	if sv.Namespace != osv.Namespace {
    99  		if !slices.Contains(sv.SharingCrossNamespace, osv.Namespace) && !slices.Contains(sv.SharingCrossNamespace, ciliumSvcLBISKCNWildward) || !slices.Contains(osv.SharingCrossNamespace, sv.Namespace) && !slices.Contains(osv.SharingCrossNamespace, ciliumSvcLBISKCNWildward) {
   100  			return false
   101  		}
   102  	}
   103  
   104  	// Compatible services don't have any overlapping ports.
   105  	// NOTE: Normally we would also consider the protocol, but the Cilium datapath can't differentiate between
   106  	// 	     protocols, so we don't either for this purpose. https://github.com/cilium/cilium/issues/9207
   107  	for _, port1 := range sv.Ports {
   108  		for _, port2 := range osv.Ports {
   109  			if port1.Port == port2.Port {
   110  				return false
   111  			}
   112  		}
   113  	}
   114  
   115  	// Compatible services have the same external traffic policy.
   116  	// If this were not the case, then we could end up in a situation directing traffic to a node which doesn't
   117  	// have the pod running on it for one of the services with an `local` external traffic policy.
   118  	if sv.ExternalTrafficPolicy != osv.ExternalTrafficPolicy {
   119  		return false
   120  	}
   121  
   122  	// If both services have a 'local' external traffic policy, then they must select the same set of pods.
   123  	// If this were not the case, then we could end up in a situation directing traffic to a node which doesn't
   124  	// have the pod running on it for one of the services.
   125  	if sv.ExternalTrafficPolicy == slim_core_v1.ServiceExternalTrafficPolicyLocal {
   126  		// If any of the two service doesn't select any pods with the selector, it likely uses an endpoints object to
   127  		// link the service to pods. LB-IPAM isn't smart enough to handle this case (yet), so we don't allow it.
   128  		if len(sv.Selector) == 0 || len(osv.Selector) == 0 {
   129  			return false
   130  		}
   131  
   132  		// If both use selectors, and they are not the same, then the services are not compatible.
   133  		if !maps.Equal(sv.Selector, osv.Selector) {
   134  			return false
   135  		}
   136  	}
   137  
   138  	// If we can't find any reason to disqualify the services, then they are compatible.
   139  	return true
   140  }
   141  
   142  func (sv *ServiceView) isSatisfied() bool {
   143  	// If the service requests specific IPs
   144  	if len(sv.RequestedIPs) > 0 {
   145  		for _, reqIP := range sv.RequestedIPs {
   146  			// If reqIP doesn't exist in the list of assigned IPs
   147  			if slices.IndexFunc(sv.Status.LoadBalancer.Ingress, func(in slim_core_v1.LoadBalancerIngress) bool {
   148  				addr, err := netip.ParseAddr(in.IP)
   149  				if err != nil {
   150  					return false
   151  				}
   152  				return addr.Compare(reqIP) == 0
   153  			}) == -1 {
   154  				return false
   155  			}
   156  		}
   157  
   158  		return true
   159  	}
   160  
   161  	// No specific requests are made, check that all requested families are assigned
   162  	hasIPv4 := false
   163  	hasIPv6 := false
   164  	for _, assigned := range sv.Status.LoadBalancer.Ingress {
   165  		if net.ParseIP(assigned.IP).To4() == nil {
   166  			hasIPv6 = true
   167  		} else {
   168  			hasIPv4 = true
   169  		}
   170  	}
   171  
   172  	// We are unsatisfied if we requested IPv4 and didn't get it or we requested IPv6 and didn't get it
   173  	unsatisfied := (sv.RequestedFamilies.IPv4 && !hasIPv4) || (sv.RequestedFamilies.IPv6 && !hasIPv6)
   174  	return !unsatisfied
   175  }
   176  
   177  // ServiceViewIP is the IP and from which range it was allocated
   178  type ServiceViewIP struct {
   179  	IP     netip.Addr
   180  	Origin *LBRange
   181  }
   182  
   183  // svcLabels clones the services labels and adds a number of internal labels which can be used to select
   184  // specific services and/or namespaces using the label selectors.
   185  func svcLabels(svc *slim_core_v1.Service) slim_labels.Set {
   186  	clone := maps.Clone(svc.Labels)
   187  	if clone == nil {
   188  		clone = make(map[string]string)
   189  	}
   190  	clone[serviceNameLabel] = svc.Name
   191  	clone[serviceNamespaceLabel] = svc.Namespace
   192  	return clone
   193  }