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 }