github.com/cilium/cilium@v1.16.2/pkg/clustermesh/endpointslicesync/service_informer.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package endpointslicesync 5 6 import ( 7 "context" 8 "fmt" 9 "maps" 10 "strings" 11 "sync/atomic" 12 13 "github.com/sirupsen/logrus" 14 v1 "k8s.io/api/core/v1" 15 "k8s.io/apimachinery/pkg/api/errors" 16 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 "k8s.io/apimachinery/pkg/labels" 18 "k8s.io/apimachinery/pkg/types" 19 "k8s.io/apimachinery/pkg/util/intstr" 20 listersv1 "k8s.io/client-go/listers/core/v1" 21 "k8s.io/client-go/tools/cache" 22 mcsapiv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 23 24 "github.com/cilium/cilium/pkg/annotation" 25 "github.com/cilium/cilium/pkg/clustermesh/common" 26 "github.com/cilium/cilium/pkg/k8s" 27 "github.com/cilium/cilium/pkg/k8s/resource" 28 slim_corev1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/api/core/v1" 29 "github.com/cilium/cilium/pkg/service/store" 30 ) 31 32 const ( 33 meshServiceNameLabel = "mesh.cilium.io/service-name" 34 meshServiceClusterLabel = "mesh.cilium.io/service-cluster" 35 ) 36 37 // meshServiceInformer uses the ClusterServices to pass fake services to the controllers 38 // based on the services in the mesh. 39 // Those services will be named with this form `$svcName-$clusterName` and contains 40 // a mcsapi source cluster label so that the underlying endpointslice will 41 // be (almost) correct. We also have a meshClient that overrides some endpointslice 42 // methods to fix back the service label name. 43 // The selector labels are always `mesh.cilium.io/service-key` and `mesh.cilium.io/service-cluster` 44 // so that the meshPodInformer can use that for his labels. 45 type meshServiceInformer struct { 46 dummyInformer 47 48 logger logrus.FieldLogger 49 globalServiceCache *common.GlobalServiceCache 50 services resource.Resource[*slim_corev1.Service] 51 serviceStore resource.Store[*slim_corev1.Service] 52 meshNodeInformer *meshNodeInformer 53 54 servicesSynced atomic.Bool 55 handler cache.ResourceEventHandler 56 } 57 58 func newNotFoundError(message string) *errors.StatusError { 59 return &errors.StatusError{ErrStatus: metav1.Status{ 60 Status: metav1.StatusFailure, 61 Reason: metav1.StatusReasonNotFound, 62 Message: message, 63 }} 64 } 65 66 func doesServiceSyncEndpointSlice(svc *slim_corev1.Service) bool { 67 value, ok := annotation.Get(svc, annotation.GlobalService) 68 if !ok || strings.ToLower(value) != "true" { 69 return false 70 } 71 72 value, ok = annotation.Get(svc, annotation.GlobalServiceSyncEndpointSlices) 73 if !ok { 74 // If the service is headless we sync the EndpointSlice by default 75 return svc.Spec.ClusterIP == v1.ClusterIPNone 76 } 77 return strings.ToLower(value) == "true" 78 } 79 80 func (i *meshServiceInformer) refreshAllCluster(svc *slim_corev1.Service) error { 81 if i.handler == nil { 82 // We don't really need to return an error here as this means that the EndpointSlice controller 83 // has not started yet and the controller will resync the initial state anyway 84 return nil 85 } 86 87 if globalSvc := i.globalServiceCache.GetGlobalService(types.NamespacedName{Name: svc.Name, Namespace: svc.Namespace}); globalSvc != nil { 88 for _, clusterSvc := range globalSvc.ClusterServices { 89 // It doesn't matter which event we trigger as the controller ends up always 90 // queuing the update the same way regardless of the event. 91 if svc, err := i.clusterSvcToSvc(clusterSvc, true); err == nil { 92 i.handler.OnAdd(svc, false) 93 } 94 } 95 } 96 97 return nil 98 } 99 100 func newMeshServiceInformer( 101 logger logrus.FieldLogger, 102 globalServiceCache *common.GlobalServiceCache, 103 services resource.Resource[*slim_corev1.Service], 104 meshNodeInformer *meshNodeInformer, 105 ) *meshServiceInformer { 106 return &meshServiceInformer{ 107 dummyInformer: dummyInformer{name: "meshServiceInformer", logger: logger}, 108 logger: logger, 109 globalServiceCache: globalServiceCache, 110 services: services, 111 meshNodeInformer: meshNodeInformer, 112 } 113 } 114 115 // toKubeServicePort use the clusterSvc to get a list of ServicePort to build 116 // the kubernetes (non slim) Service. Note that we cannot use the slim Service to get this 117 // as the slim Service trims the TargetPort which we needs inside the EndpointSliceReconciler 118 func toKubeServicePort(clusterSvc *store.ClusterService) []v1.ServicePort { 119 // Merge all the port config into one to get all the possible ports 120 globalPortConfig := store.PortConfiguration{} 121 for _, portConfig := range clusterSvc.Backends { 122 for name, l4Addr := range portConfig { 123 globalPortConfig[name] = l4Addr 124 } 125 } 126 127 // Get the ServicePort from the PortConfig 128 regularServicePorts := make([]v1.ServicePort, 0, len(globalPortConfig)) 129 for name, l4Addr := range globalPortConfig { 130 regularServicePorts = append(regularServicePorts, v1.ServicePort{ 131 Name: name, 132 Protocol: v1.Protocol(l4Addr.Protocol), 133 TargetPort: intstr.FromInt(int(l4Addr.Port)), 134 }) 135 } 136 return regularServicePorts 137 } 138 139 // toKubeIpFamilies convert the ipFamilies from the Cilium slim Service type 140 // to the regular Kubernetes Service type 141 func toKubeIpFamilies(ipFamilies []slim_corev1.IPFamily) []v1.IPFamily { 142 regularIpFamilies := make([]v1.IPFamily, len(ipFamilies)) 143 for i, ipFamily := range ipFamilies { 144 regularIpFamilies[i] = v1.IPFamily(ipFamily) 145 } 146 return regularIpFamilies 147 } 148 149 func (i *meshServiceInformer) clusterSvcToSvc(clusterSvc *store.ClusterService, force bool) (*v1.Service, error) { 150 if i.serviceStore == nil { 151 return nil, fmt.Errorf("service informer not started yet") 152 } 153 154 svc, exists, err := i.serviceStore.GetByKey(resource.Key{Name: clusterSvc.Name, Namespace: clusterSvc.Namespace}) 155 if err != nil { 156 return nil, err 157 } 158 159 if !exists { 160 return nil, newNotFoundError(fmt.Sprintf("service '%s' not found", clusterSvc.NamespaceServiceName())) 161 } 162 163 if !force && !doesServiceSyncEndpointSlice(svc) { 164 return nil, newNotFoundError(fmt.Sprintf("service '%s' does not have sync endpoint slice annotation", clusterSvc.NamespaceServiceName())) 165 } 166 167 labels := maps.Clone(svc.Labels) 168 if labels == nil { 169 labels = map[string]string{} 170 } 171 maps.Copy(labels, map[string]string{ 172 meshRealServiceNameLabel: clusterSvc.Name, 173 mcsapiv1alpha1.LabelSourceCluster: clusterSvc.Cluster, 174 }) 175 176 return &v1.Service{ 177 ObjectMeta: metav1.ObjectMeta{ 178 Name: clusterSvc.Name + "-" + clusterSvc.Cluster, 179 UID: svc.UID, 180 Namespace: clusterSvc.Namespace, 181 Labels: labels, 182 }, 183 Spec: v1.ServiceSpec{ 184 Ports: toKubeServicePort(clusterSvc), 185 Selector: map[string]string{ 186 meshServiceNameLabel: clusterSvc.Name, 187 meshServiceClusterLabel: clusterSvc.Cluster, 188 }, 189 ClusterIP: svc.Spec.ClusterIP, 190 ClusterIPs: svc.Spec.ClusterIPs, 191 Type: v1.ServiceType(svc.Spec.Type), 192 IPFamilies: toKubeIpFamilies(svc.Spec.IPFamilies), 193 }, 194 }, nil 195 } 196 197 type meshServiceLister struct { 198 informer *meshServiceInformer 199 namespace string 200 } 201 202 // List returns the matrix of all the local services and all the remote clusters. 203 // This is not similar to what does the Get method. For instance, List may returns 204 // services that could not be found on a call to the Get method. 205 // By doing that we can ensure that the controller reconciliation is called 206 // on every possible remote services especially the one that are deleted while 207 // we still have some EndpointSlices locally (which won't be cleared by the 208 // OwnerReference mechanism since our actual local service is not deleted). 209 func (l meshServiceLister) List(selector labels.Selector) ([]*v1.Service, error) { 210 reqs, _ := selector.Requirements() 211 if !selector.Empty() { 212 return nil, fmt.Errorf("meshServiceInformer only supports listing everything as requirements: %s", reqs) 213 } 214 215 originalSvcs, err := l.informer.serviceStore.ByIndex(k8s.NamespaceIndex, l.namespace) 216 if err != nil { 217 return nil, err 218 } 219 220 clusters := l.informer.meshNodeInformer.ListClusters() 221 var svcs []*v1.Service 222 for _, svc := range originalSvcs { 223 for _, cluster := range clusters { 224 dummyClusterSvc := &store.ClusterService{ 225 Cluster: cluster, 226 Name: svc.Name, 227 Namespace: l.namespace, 228 } 229 if svc, err := l.informer.clusterSvcToSvc(dummyClusterSvc, false); err == nil { 230 svcs = append(svcs, svc) 231 } 232 } 233 } 234 235 return svcs, nil 236 } 237 238 // Get attempts to retrieve a *store.ClusterService object by separating the 239 // service name in the full service name and then convert it to a regular 240 // Kubernetes Service. 241 func (l meshServiceLister) Get(name string) (*v1.Service, error) { 242 // TODO: We could try to use an illegal character to separate the service name and 243 // the cluster name 244 posClusterName := len(name) 245 for { 246 posClusterName = strings.LastIndex(name[:posClusterName], "-") 247 if posClusterName == -1 { 248 break 249 } 250 251 svcName := name[:posClusterName] 252 clusterName := name[posClusterName+1:] 253 clusterSvc := l.informer.globalServiceCache.GetService(types.NamespacedName{Name: svcName, Namespace: l.namespace}, clusterName) 254 255 if clusterSvc == nil { 256 continue 257 } 258 return l.informer.clusterSvcToSvc(clusterSvc, false) 259 } 260 261 return nil, newNotFoundError(fmt.Sprintf("cannot find cluster service with name '%s'", name)) 262 } 263 264 func (i *meshServiceInformer) AddEventHandler(handler cache.ResourceEventHandler) (cache.ResourceEventHandlerRegistration, error) { 265 i.handler = handler 266 return i, nil 267 } 268 269 func (i *meshServiceInformer) HasSynced() bool { 270 return i.servicesSynced.Load() 271 } 272 273 func (i *meshServiceInformer) Start(ctx context.Context) error { 274 var err error 275 if i.serviceStore, err = i.services.Store(ctx); err != nil { 276 return err 277 } 278 279 go func() { 280 for event := range i.services.Events(ctx) { 281 var err error 282 switch event.Kind { 283 case resource.Sync: 284 i.logger.Debug("Local services are synced") 285 i.servicesSynced.Store(true) 286 case resource.Upsert: 287 err = i.refreshAllCluster(event.Object) 288 case resource.Delete: 289 err = i.refreshAllCluster(event.Object) 290 } 291 event.Done(err) 292 } 293 }() 294 return nil 295 } 296 297 func (i *meshServiceInformer) Services(namespace string) listersv1.ServiceNamespaceLister { 298 return &meshServiceLister{informer: i, namespace: namespace} 299 } 300 func (i *meshServiceInformer) Informer() cache.SharedIndexInformer { 301 return i 302 } 303 func (i *meshServiceInformer) Lister() listersv1.ServiceLister { 304 return i 305 } 306 307 func (i *meshServiceInformer) List(selector labels.Selector) (ret []*v1.Service, err error) { 308 i.logger.Error("called not implemented function meshServiceInformer.List") 309 return nil, nil 310 }