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  }