github.com/cilium/cilium@v1.16.2/pkg/k8s/watchers/service.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package watchers 5 6 import ( 7 "context" 8 "errors" 9 "net" 10 "sync/atomic" 11 12 "github.com/cilium/hive/cell" 13 "github.com/sirupsen/logrus" 14 15 agentK8s "github.com/cilium/cilium/daemon/k8s" 16 "github.com/cilium/cilium/pkg/bgp/speaker" 17 "github.com/cilium/cilium/pkg/cidr" 18 cmtypes "github.com/cilium/cilium/pkg/clustermesh/types" 19 "github.com/cilium/cilium/pkg/ip" 20 "github.com/cilium/cilium/pkg/k8s" 21 "github.com/cilium/cilium/pkg/k8s/resource" 22 slim_corev1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/api/core/v1" 23 k8sSynced "github.com/cilium/cilium/pkg/k8s/synced" 24 "github.com/cilium/cilium/pkg/k8s/watchers/resources" 25 "github.com/cilium/cilium/pkg/loadbalancer" 26 "github.com/cilium/cilium/pkg/lock" 27 "github.com/cilium/cilium/pkg/logging" 28 "github.com/cilium/cilium/pkg/logging/logfields" 29 "github.com/cilium/cilium/pkg/metrics" 30 "github.com/cilium/cilium/pkg/option" 31 "github.com/cilium/cilium/pkg/redirectpolicy" 32 "github.com/cilium/cilium/pkg/safetime" 33 "github.com/cilium/cilium/pkg/service" 34 "github.com/cilium/cilium/pkg/time" 35 ) 36 37 type k8sServiceWatcherParams struct { 38 cell.In 39 40 K8sEventReporter *K8sEventReporter 41 42 Resources agentK8s.Resources 43 K8sResourceSynced *k8sSynced.Resources 44 K8sAPIGroups *k8sSynced.APIGroups 45 46 ServiceCache *k8s.ServiceCache 47 ServiceManager service.ServiceManager 48 LRPManager *redirectpolicy.Manager 49 MetalLBBgpSpeaker speaker.MetalLBBgpSpeaker 50 } 51 52 func newK8sServiceWatcher(params k8sServiceWatcherParams) *K8sServiceWatcher { 53 return &K8sServiceWatcher{ 54 k8sEventReporter: params.K8sEventReporter, 55 k8sResourceSynced: params.K8sResourceSynced, 56 k8sAPIGroups: params.K8sAPIGroups, 57 resources: params.Resources, 58 k8sSvcCache: params.ServiceCache, 59 svcManager: params.ServiceManager, 60 redirectPolicyManager: params.LRPManager, 61 bgpSpeakerManager: params.MetalLBBgpSpeaker, 62 stop: make(chan struct{}), 63 } 64 } 65 66 type K8sServiceWatcher struct { 67 k8sEventReporter *K8sEventReporter 68 // k8sResourceSynced maps a resource name to a channel. Once the given 69 // resource name is synchronized with k8s, the channel for which that 70 // resource name maps to is closed. 71 k8sResourceSynced *k8sSynced.Resources 72 // k8sAPIGroups is a set of k8s API in use. They are setup in watchers, 73 // and may be disabled while the agent runs. 74 k8sAPIGroups *k8sSynced.APIGroups 75 resources agentK8s.Resources 76 77 k8sSvcCache *k8s.ServiceCache 78 svcManager svcManager 79 redirectPolicyManager redirectPolicyManager 80 bgpSpeakerManager bgpSpeakerManager 81 82 stop chan struct{} 83 } 84 85 func (k *K8sServiceWatcher) servicesInit() { 86 var synced atomic.Bool 87 swgSvcs := lock.NewStoppableWaitGroup() 88 89 k.k8sResourceSynced.BlockWaitGroupToSyncResources( 90 k.stop, 91 swgSvcs, 92 func() bool { return synced.Load() }, 93 resources.K8sAPIGroupServiceV1Core, 94 ) 95 go k.serviceEventLoop(&synced, swgSvcs) 96 97 k.k8sAPIGroups.AddAPI(resources.K8sAPIGroupServiceV1Core) 98 } 99 100 func (k *K8sServiceWatcher) stopWatcher() { 101 close(k.stop) 102 } 103 104 func (k *K8sServiceWatcher) serviceEventLoop(synced *atomic.Bool, swg *lock.StoppableWaitGroup) { 105 apiGroup := resources.K8sAPIGroupServiceV1Core 106 ctx, cancel := context.WithCancel(context.Background()) 107 defer cancel() 108 109 events := k.resources.Services.Events(ctx) 110 for { 111 select { 112 case <-k.stop: 113 cancel() 114 case event, ok := <-events: 115 if !ok { 116 return 117 } 118 switch event.Kind { 119 case resource.Sync: 120 synced.Store(true) 121 case resource.Upsert: 122 svc := event.Object 123 k.k8sResourceSynced.SetEventTimestamp(apiGroup) 124 k.upsertK8sServiceV1(svc, swg) 125 case resource.Delete: 126 svc := event.Object 127 k.k8sResourceSynced.SetEventTimestamp(apiGroup) 128 k.deleteK8sServiceV1(svc, swg) 129 } 130 event.Done(nil) 131 } 132 } 133 } 134 135 func (k *K8sServiceWatcher) upsertK8sServiceV1(svc *slim_corev1.Service, swg *lock.StoppableWaitGroup) { 136 svcID := k.k8sSvcCache.UpdateService(svc, swg) 137 if option.Config.EnableLocalRedirectPolicy { 138 if svc.Spec.Type == slim_corev1.ServiceTypeClusterIP { 139 // The local redirect policies currently support services of type 140 // clusterIP only. 141 k.redirectPolicyManager.OnAddService(svcID) 142 } 143 } 144 k.bgpSpeakerManager.OnUpdateService(svc) 145 } 146 147 func (k *K8sServiceWatcher) deleteK8sServiceV1(svc *slim_corev1.Service, swg *lock.StoppableWaitGroup) { 148 k.k8sSvcCache.DeleteService(svc, swg) 149 svcID := k8s.ParseServiceID(svc) 150 if option.Config.EnableLocalRedirectPolicy { 151 if svc.Spec.Type == slim_corev1.ServiceTypeClusterIP { 152 k.redirectPolicyManager.OnDeleteService(svcID) 153 } 154 } 155 k.bgpSpeakerManager.OnDeleteService(svc) 156 } 157 158 func (k *K8sServiceWatcher) k8sServiceHandler() { 159 eventHandler := func(event k8s.ServiceEvent) { 160 defer func(startTime time.Time) { 161 event.SWG.Done() 162 k.k8sServiceEventProcessed(event.Action.String(), startTime) 163 }(time.Now()) 164 165 svc := event.Service 166 167 scopedLog := log.WithFields(logrus.Fields{ 168 logfields.K8sSvcName: event.ID.Name, 169 logfields.K8sNamespace: event.ID.Namespace, 170 }) 171 172 if logging.CanLogAt(scopedLog.Logger, logrus.DebugLevel) { 173 scopedLog.WithFields(logrus.Fields{ 174 "action": event.Action.String(), 175 "service": event.Service.String(), 176 "old-service": event.OldService.String(), 177 "endpoints": event.Endpoints.String(), 178 "old-endpoints": event.OldEndpoints.String(), 179 }).Debug("Kubernetes service definition changed") 180 } 181 182 switch event.Action { 183 case k8s.UpdateService: 184 k.addK8sSVCs(event.ID, event.OldService, svc, event.Endpoints) 185 case k8s.DeleteService: 186 k.delK8sSVCs(event.ID, event.Service) 187 } 188 } 189 for { 190 select { 191 case <-k.stop: 192 return 193 case event, ok := <-k.k8sSvcCache.Events: 194 if !ok { 195 return 196 } 197 eventHandler(event) 198 } 199 } 200 } 201 202 func (k *K8sServiceWatcher) RunK8sServiceHandler() { 203 go k.k8sServiceHandler() 204 } 205 206 func (k *K8sServiceWatcher) delK8sSVCs(svc k8s.ServiceID, svcInfo *k8s.Service) { 207 // Headless services do not need any datapath implementation 208 if svcInfo.IsHeadless { 209 return 210 } 211 212 scopedLog := log.WithFields(logrus.Fields{ 213 logfields.K8sSvcName: svc.Name, 214 logfields.K8sNamespace: svc.Namespace, 215 }) 216 217 repPorts := svcInfo.UniquePorts() 218 219 frontends := []*loadbalancer.L3n4Addr{} 220 221 for portName, svcPort := range svcInfo.Ports { 222 if !repPorts[svcPort.Port] { 223 continue 224 } 225 repPorts[svcPort.Port] = false 226 227 for _, feIP := range svcInfo.FrontendIPs { 228 fe := loadbalancer.NewL3n4Addr(svcPort.Protocol, cmtypes.MustAddrClusterFromIP(feIP), svcPort.Port, loadbalancer.ScopeExternal) 229 frontends = append(frontends, fe) 230 } 231 232 for _, nodePortFE := range svcInfo.NodePorts[portName] { 233 frontends = append(frontends, &nodePortFE.L3n4Addr) 234 if svcInfo.ExtTrafficPolicy == loadbalancer.SVCTrafficPolicyLocal || svcInfo.IntTrafficPolicy == loadbalancer.SVCTrafficPolicyLocal { 235 cpFE := nodePortFE.L3n4Addr.DeepCopy() 236 cpFE.Scope = loadbalancer.ScopeInternal 237 frontends = append(frontends, cpFE) 238 } 239 } 240 241 for _, k8sExternalIP := range svcInfo.K8sExternalIPs { 242 frontends = append(frontends, loadbalancer.NewL3n4Addr(svcPort.Protocol, cmtypes.MustAddrClusterFromIP(k8sExternalIP), svcPort.Port, loadbalancer.ScopeExternal)) 243 } 244 245 for _, ip := range svcInfo.LoadBalancerIPs { 246 addrCluster := cmtypes.MustAddrClusterFromIP(ip) 247 frontends = append(frontends, loadbalancer.NewL3n4Addr(svcPort.Protocol, addrCluster, svcPort.Port, loadbalancer.ScopeExternal)) 248 if svcInfo.ExtTrafficPolicy == loadbalancer.SVCTrafficPolicyLocal || svcInfo.IntTrafficPolicy == loadbalancer.SVCTrafficPolicyLocal { 249 frontends = append(frontends, loadbalancer.NewL3n4Addr(svcPort.Protocol, addrCluster, svcPort.Port, loadbalancer.ScopeInternal)) 250 } 251 } 252 } 253 254 for _, fe := range frontends { 255 if found, err := k.svcManager.DeleteService(*fe); err != nil { 256 scopedLog.WithError(err).WithField(logfields.Object, logfields.Repr(fe)). 257 Warn("Error deleting service by frontend") 258 } else if !found { 259 scopedLog.WithField(logfields.Object, logfields.Repr(fe)).Warn("service not found") 260 } else { 261 scopedLog.Debugf("# cilium lb delete-service %s %d 0", fe.AddrCluster.String(), fe.Port) 262 } 263 } 264 } 265 266 func genCartesianProduct( 267 fe net.IP, 268 twoScopes bool, 269 svcType loadbalancer.SVCType, 270 ports map[loadbalancer.FEPortName]*loadbalancer.L4Addr, 271 bes *k8s.Endpoints, 272 ) []loadbalancer.SVC { 273 var svcSize int 274 275 // For externalTrafficPolicy=Local xor internalTrafficPolicy=Local we 276 // add both external and internal scoped frontends, hence twice the size 277 // for only this case. 278 if twoScopes && 279 (svcType == loadbalancer.SVCTypeLoadBalancer || svcType == loadbalancer.SVCTypeNodePort) { 280 svcSize = len(ports) * 2 281 } else { 282 svcSize = len(ports) 283 } 284 285 svcs := make([]loadbalancer.SVC, 0, svcSize) 286 feFamilyIPv6 := ip.IsIPv6(fe) 287 288 for fePortName, fePort := range ports { 289 var besValues []*loadbalancer.Backend 290 for addrCluster, backend := range bes.Backends { 291 if backendPort := backend.Ports[string(fePortName)]; backendPort != nil && feFamilyIPv6 == addrCluster.Is6() { 292 backendState := loadbalancer.BackendStateActive 293 if backend.Terminating { 294 backendState = loadbalancer.BackendStateTerminating 295 } 296 besValues = append(besValues, &loadbalancer.Backend{ 297 FEPortName: string(fePortName), 298 NodeName: backend.NodeName, 299 ZoneID: option.Config.GetZoneID(backend.Zone), 300 L3n4Addr: loadbalancer.L3n4Addr{ 301 AddrCluster: addrCluster, 302 L4Addr: *backendPort, 303 }, 304 State: backendState, 305 Preferred: loadbalancer.Preferred(backend.Preferred), 306 Weight: loadbalancer.DefaultBackendWeight, 307 }) 308 } 309 } 310 311 addrCluster := cmtypes.MustAddrClusterFromIP(fe) 312 313 // External scoped entry - when external and internal policies are the same. 314 svcs = append(svcs, 315 loadbalancer.SVC{ 316 Frontend: loadbalancer.L3n4AddrID{ 317 L3n4Addr: loadbalancer.L3n4Addr{ 318 AddrCluster: addrCluster, 319 L4Addr: loadbalancer.L4Addr{ 320 Protocol: fePort.Protocol, 321 Port: fePort.Port, 322 }, 323 Scope: loadbalancer.ScopeExternal, 324 }, 325 ID: loadbalancer.ID(0), 326 }, 327 Backends: besValues, 328 Type: svcType, 329 }) 330 331 // Internal scoped entry - when only one of traffic policies is Local. 332 if svcSize > len(ports) { 333 svcs = append(svcs, 334 loadbalancer.SVC{ 335 Frontend: loadbalancer.L3n4AddrID{ 336 L3n4Addr: loadbalancer.L3n4Addr{ 337 AddrCluster: addrCluster, 338 L4Addr: loadbalancer.L4Addr{ 339 Protocol: fePort.Protocol, 340 Port: fePort.Port, 341 }, 342 Scope: loadbalancer.ScopeInternal, 343 }, 344 ID: loadbalancer.ID(0), 345 }, 346 Backends: besValues, 347 Type: svcType, 348 }) 349 } 350 } 351 return svcs 352 } 353 354 // datapathSVCs returns all services that should be set in the datapath. 355 func datapathSVCs(svc *k8s.Service, endpoints *k8s.Endpoints) (svcs []loadbalancer.SVC) { 356 uniqPorts := svc.UniquePorts() 357 358 clusterIPPorts := map[loadbalancer.FEPortName]*loadbalancer.L4Addr{} 359 for fePortName, fePort := range svc.Ports { 360 if !uniqPorts[fePort.Port] { 361 continue 362 } 363 uniqPorts[fePort.Port] = false 364 clusterIPPorts[fePortName] = fePort 365 } 366 367 twoScopes := (svc.ExtTrafficPolicy == loadbalancer.SVCTrafficPolicyLocal) != (svc.IntTrafficPolicy == loadbalancer.SVCTrafficPolicyLocal) 368 369 for _, frontendIP := range svc.FrontendIPs { 370 dpSVC := genCartesianProduct(frontendIP, twoScopes, loadbalancer.SVCTypeClusterIP, clusterIPPorts, endpoints) 371 svcs = append(svcs, dpSVC...) 372 } 373 374 for _, ip := range svc.LoadBalancerIPs { 375 dpSVC := genCartesianProduct(ip, twoScopes, loadbalancer.SVCTypeLoadBalancer, clusterIPPorts, endpoints) 376 svcs = append(svcs, dpSVC...) 377 } 378 379 for _, k8sExternalIP := range svc.K8sExternalIPs { 380 dpSVC := genCartesianProduct(k8sExternalIP, twoScopes, loadbalancer.SVCTypeExternalIPs, clusterIPPorts, endpoints) 381 svcs = append(svcs, dpSVC...) 382 } 383 384 for fePortName := range clusterIPPorts { 385 for _, nodePortFE := range svc.NodePorts[fePortName] { 386 nodePortPorts := map[loadbalancer.FEPortName]*loadbalancer.L4Addr{ 387 fePortName: &nodePortFE.L4Addr, 388 } 389 dpSVC := genCartesianProduct(nodePortFE.AddrCluster.Addr().AsSlice(), twoScopes, loadbalancer.SVCTypeNodePort, nodePortPorts, endpoints) 390 svcs = append(svcs, dpSVC...) 391 } 392 } 393 394 lbSrcRanges := make([]*cidr.CIDR, 0, len(svc.LoadBalancerSourceRanges)) 395 for _, cidr := range svc.LoadBalancerSourceRanges { 396 lbSrcRanges = append(lbSrcRanges, cidr) 397 } 398 399 // apply common service properties 400 for i := range svcs { 401 svcs[i].ExtTrafficPolicy = svc.ExtTrafficPolicy 402 svcs[i].IntTrafficPolicy = svc.IntTrafficPolicy 403 svcs[i].HealthCheckNodePort = svc.HealthCheckNodePort 404 svcs[i].SessionAffinity = svc.SessionAffinity 405 svcs[i].SessionAffinityTimeoutSec = svc.SessionAffinityTimeoutSec 406 if svcs[i].Type == loadbalancer.SVCTypeLoadBalancer { 407 svcs[i].LoadBalancerSourceRanges = lbSrcRanges 408 } 409 } 410 411 return svcs 412 } 413 414 // hashSVCMap returns a mapping of all frontend's hash to the its corresponded 415 // value. 416 func hashSVCMap(svcs []loadbalancer.SVC) map[string]loadbalancer.L3n4Addr { 417 m := map[string]loadbalancer.L3n4Addr{} 418 for _, svc := range svcs { 419 m[svc.Frontend.L3n4Addr.Hash()] = svc.Frontend.L3n4Addr 420 } 421 return m 422 } 423 424 func (k *K8sServiceWatcher) addK8sSVCs(svcID k8s.ServiceID, oldSvc, svc *k8s.Service, endpoints *k8s.Endpoints) { 425 // Headless services do not need any datapath implementation 426 if svc.IsHeadless { 427 return 428 } 429 430 scopedLog := log.WithFields(logrus.Fields{ 431 logfields.K8sSvcName: svcID.Name, 432 logfields.K8sNamespace: svcID.Namespace, 433 }) 434 435 svcs := datapathSVCs(svc, endpoints) 436 svcMap := hashSVCMap(svcs) 437 438 if oldSvc != nil { 439 // If we have oldService then we need to detect which frontends 440 // are no longer in the updated service and delete them in the datapath. 441 442 oldSVCs := datapathSVCs(oldSvc, endpoints) 443 oldSVCMap := hashSVCMap(oldSVCs) 444 445 for svcHash, oldSvc := range oldSVCMap { 446 if _, ok := svcMap[svcHash]; !ok { 447 if found, err := k.svcManager.DeleteService(oldSvc); err != nil { 448 scopedLog.WithError(err).WithField(logfields.Object, logfields.Repr(oldSvc)). 449 Warn("Error deleting service by frontend") 450 } else if !found { 451 scopedLog.WithField(logfields.Object, logfields.Repr(oldSvc)).Warn("service not found") 452 } else { 453 scopedLog.Debugf("# cilium lb delete-service %s %d 0", oldSvc.AddrCluster.String(), oldSvc.Port) 454 } 455 } 456 } 457 } 458 459 for _, dpSvc := range svcs { 460 p := &loadbalancer.SVC{ 461 Frontend: dpSvc.Frontend, 462 Backends: dpSvc.Backends, 463 Type: dpSvc.Type, 464 ExtTrafficPolicy: dpSvc.ExtTrafficPolicy, 465 IntTrafficPolicy: dpSvc.IntTrafficPolicy, 466 SessionAffinity: dpSvc.SessionAffinity, 467 SessionAffinityTimeoutSec: dpSvc.SessionAffinityTimeoutSec, 468 HealthCheckNodePort: dpSvc.HealthCheckNodePort, 469 LoadBalancerSourceRanges: dpSvc.LoadBalancerSourceRanges, 470 Name: loadbalancer.ServiceName{ 471 Name: svcID.Name, 472 Namespace: svcID.Namespace, 473 Cluster: svcID.Cluster, 474 }, 475 } 476 if _, _, err := k.svcManager.UpsertService(p); err != nil { 477 if errors.Is(err, service.NewErrLocalRedirectServiceExists(p.Frontend, p.Name)) { 478 scopedLog.WithError(err).Debug("Error while inserting service in LB map") 479 } else { 480 scopedLog.WithError(err).Error("Error while inserting service in LB map") 481 } 482 } 483 } 484 } 485 486 // k8sServiceEventProcessed is called to do metrics accounting the duration to program the service. 487 func (k *K8sServiceWatcher) k8sServiceEventProcessed(action string, startTime time.Time) { 488 duration, _ := safetime.TimeSinceSafe(startTime, log) 489 metrics.ServiceImplementationDelay.WithLabelValues(action).Observe(duration.Seconds()) 490 }