github.com/cilium/cilium@v1.16.2/pkg/clustermesh/mcsapi/service_controller.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package mcsapi
     5  
     6  import (
     7  	"context"
     8  	"crypto/sha256"
     9  	"encoding/base32"
    10  	"maps"
    11  	"strings"
    12  
    13  	"github.com/sirupsen/logrus"
    14  	corev1 "k8s.io/api/core/v1"
    15  	k8sApiErrors "k8s.io/apimachinery/pkg/api/errors"
    16  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    17  	"k8s.io/apimachinery/pkg/types"
    18  	ctrl "sigs.k8s.io/controller-runtime"
    19  	"sigs.k8s.io/controller-runtime/pkg/client"
    20  	"sigs.k8s.io/controller-runtime/pkg/handler"
    21  	mcsapiv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1"
    22  	mcsapicontrollers "sigs.k8s.io/mcs-api/pkg/controllers"
    23  
    24  	controllerruntime "github.com/cilium/cilium/operator/pkg/controller-runtime"
    25  	"github.com/cilium/cilium/pkg/annotation"
    26  )
    27  
    28  const (
    29  	kindServiceImport = "ServiceImport"
    30  	kindServiceExport = "ServiceExport"
    31  )
    32  
    33  // mcsAPIServiceReconciler is a controller that creates a derived service from
    34  // a ServiceImport and ServiceExport objects. The derived Service is created
    35  // with the Cilium annotations to mark it as a global Service so that we can
    36  // take advantage of the existing clustermesh features for the MCS API Support.
    37  type mcsAPIServiceReconciler struct {
    38  	client.Client
    39  	Logger logrus.FieldLogger
    40  
    41  	clusterName string
    42  }
    43  
    44  func newMCSAPIServiceReconciler(mgr ctrl.Manager, logger logrus.FieldLogger, clusterName string) *mcsAPIServiceReconciler {
    45  	return &mcsAPIServiceReconciler{
    46  		Client:      mgr.GetClient(),
    47  		Logger:      logger,
    48  		clusterName: clusterName,
    49  	}
    50  }
    51  
    52  func getOwnerReferenceName(refs []metav1.OwnerReference, apiVersion string, kind string) string {
    53  	for _, ref := range refs {
    54  		if ref.APIVersion != apiVersion {
    55  			continue
    56  		}
    57  		if ref.Kind == kind {
    58  			return ref.Name
    59  		}
    60  	}
    61  	return ""
    62  }
    63  
    64  func getMCSAPIOwner(refs []metav1.OwnerReference) string {
    65  	if ref := getOwnerReferenceName(refs, mcsapiv1alpha1.GroupVersion.String(), kindServiceImport); ref != "" {
    66  		return ref
    67  	}
    68  	if ref := getOwnerReferenceName(refs, mcsapiv1alpha1.GroupVersion.String(), kindServiceExport); ref != "" {
    69  		return ref
    70  	}
    71  	return ""
    72  }
    73  
    74  // derivedName derive the original name in the format "derived-$hash".
    75  // This function was taken from the mcs-api repo: https://github.com/kubernetes-sigs/mcs-api/blob/4231f56e5ff985676b8ac99034b05609cf4a9e0d/pkg/controllers/common.go#L39
    76  func derivedName(name types.NamespacedName) string {
    77  	hash := sha256.New()
    78  	hash.Write([]byte(name.String()))
    79  	return "derived-" + strings.ToLower(base32.HexEncoding.WithPadding(base32.NoPadding).EncodeToString(hash.Sum(nil)))[:10]
    80  }
    81  
    82  func servicePorts(svcImport *mcsapiv1alpha1.ServiceImport) []corev1.ServicePort {
    83  	ports := make([]corev1.ServicePort, 0, len(svcImport.Spec.Ports))
    84  	for _, port := range svcImport.Spec.Ports {
    85  		ports = append(ports, corev1.ServicePort{
    86  			Name:        port.Name,
    87  			Protocol:    port.Protocol,
    88  			AppProtocol: port.AppProtocol,
    89  			Port:        port.Port,
    90  		})
    91  	}
    92  	return ports
    93  }
    94  
    95  func addOwnerReference(svc *corev1.Service, objOwner client.Object) {
    96  	apiVersion := objOwner.GetObjectKind().GroupVersionKind().GroupVersion().String()
    97  	kind := objOwner.GetObjectKind().GroupVersionKind().Kind
    98  
    99  	svc.OwnerReferences = append(svc.OwnerReferences,
   100  		metav1.OwnerReference{
   101  			Name:       objOwner.GetName(),
   102  			Kind:       kind,
   103  			APIVersion: apiVersion,
   104  			UID:        objOwner.GetUID(),
   105  		})
   106  }
   107  
   108  func (r *mcsAPIServiceReconciler) addServiceImportDerivedAnnotation(ctx context.Context, svcImport *mcsapiv1alpha1.ServiceImport, derivedServiceName string) error {
   109  	if svcImport == nil {
   110  		return nil
   111  	}
   112  	if svcImport.Annotations == nil {
   113  		svcImport.Annotations = map[string]string{}
   114  	}
   115  	if svcImport.Annotations[mcsapicontrollers.DerivedServiceAnnotation] != derivedServiceName {
   116  		svcImport.Annotations[mcsapicontrollers.DerivedServiceAnnotation] = derivedServiceName
   117  		if err := r.Client.Update(ctx, svcImport); err != nil {
   118  			return err
   119  		}
   120  	}
   121  	return nil
   122  }
   123  
   124  // getDerivedService get the derived service if it exist or else a minimally constructed
   125  // service. If the base service has the wrong headlessness it will be auto deleted as well.
   126  func (r *mcsAPIServiceReconciler) getBaseDerivedService(
   127  	ctx context.Context,
   128  	req ctrl.Request,
   129  	derivedServiceName string,
   130  	svcImport *mcsapiv1alpha1.ServiceImport,
   131  ) (*corev1.Service, bool, error) {
   132  	isHeadless := false
   133  	if svcImport != nil {
   134  		isHeadless = svcImport.Spec.Type == mcsapiv1alpha1.Headless
   135  	}
   136  
   137  	svcBase := &corev1.Service{
   138  		ObjectMeta: metav1.ObjectMeta{
   139  			Namespace: req.Namespace,
   140  			Name:      derivedServiceName,
   141  		},
   142  		Spec: corev1.ServiceSpec{
   143  			Type: corev1.ServiceTypeClusterIP,
   144  		},
   145  	}
   146  	if isHeadless {
   147  		svcBase.Spec.ClusterIP = corev1.ClusterIPNone
   148  	}
   149  
   150  	var svc corev1.Service
   151  	if err := r.Client.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: derivedServiceName}, &svc); err != nil {
   152  		if !k8sApiErrors.IsNotFound(err) {
   153  			return nil, false, err
   154  		}
   155  		return svcBase, false, nil
   156  	}
   157  
   158  	if isHeadless != (svc.Spec.ClusterIP == corev1.ClusterIPNone) {
   159  		// We need to delete the derived service first if we need to switch
   160  		// to/from headless on a Service that already exists.
   161  		if err := r.Client.Delete(ctx, &svc); err != nil {
   162  			return nil, false, err
   163  		}
   164  		return svcBase, false, nil
   165  	}
   166  	return &svc, true, nil
   167  }
   168  
   169  func (r *mcsAPIServiceReconciler) getLocalService(ctx context.Context, req ctrl.Request) (*corev1.Service, error) {
   170  	var svc corev1.Service
   171  	if err := r.Client.Get(ctx, req.NamespacedName, &svc); err != nil {
   172  		return nil, err
   173  	}
   174  	return &svc, nil
   175  }
   176  
   177  func (r *mcsAPIServiceReconciler) getSvcExport(ctx context.Context, req ctrl.Request) (*mcsapiv1alpha1.ServiceExport, error) {
   178  	var svcExport mcsapiv1alpha1.ServiceExport
   179  	if err := r.Client.Get(ctx, req.NamespacedName, &svcExport); err != nil {
   180  		if k8sApiErrors.IsNotFound(err) {
   181  			return nil, nil
   182  		}
   183  		return nil, err
   184  	}
   185  	return &svcExport, nil
   186  }
   187  
   188  func (r *mcsAPIServiceReconciler) getSvcImport(ctx context.Context, req ctrl.Request) (*mcsapiv1alpha1.ServiceImport, error) {
   189  	var svcImport mcsapiv1alpha1.ServiceImport
   190  	if err := r.Client.Get(ctx, req.NamespacedName, &svcImport); err != nil {
   191  		if k8sApiErrors.IsNotFound(err) {
   192  			return nil, nil
   193  		}
   194  		return nil, err
   195  	}
   196  	return &svcImport, nil
   197  }
   198  
   199  func (r *mcsAPIServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
   200  	svcImport, err := r.getSvcImport(ctx, req)
   201  	if err != nil {
   202  		return controllerruntime.Fail(err)
   203  	}
   204  	svcExport, err := r.getSvcExport(ctx, req)
   205  	if err != nil {
   206  		return controllerruntime.Fail(err)
   207  	}
   208  
   209  	if svcExport == nil && svcImport == nil {
   210  		return controllerruntime.Success()
   211  	}
   212  
   213  	derivedServiceName := derivedName(req.NamespacedName)
   214  	svc, svcExists, err := r.getBaseDerivedService(ctx, req, derivedServiceName, svcImport)
   215  	if err != nil {
   216  		return controllerruntime.Fail(err)
   217  	}
   218  
   219  	svc.Spec.Ports = []corev1.ServicePort{}
   220  	svc.Spec.Selector = map[string]string{}
   221  	svc.OwnerReferences = []metav1.OwnerReference{}
   222  	svc.Annotations = map[string]string{}
   223  	svc.Labels = map[string]string{}
   224  
   225  	localSvc, err := r.getLocalService(ctx, req)
   226  	if err != nil && (!k8sApiErrors.IsNotFound(err) || svcExport != nil) {
   227  		return controllerruntime.Fail(err)
   228  	}
   229  
   230  	// Copy the local Service selector to let kube-controller-manager do
   231  	// the actual syncing of the endpoints.
   232  	// This has the drawback that this implementation doesn't
   233  	// support the endpoints created with the `kubernetes.io/service-name`
   234  	// label without any pod backing them (i.e.: endpoints created manually
   235  	// or by some third party tooling).
   236  	if localSvc != nil {
   237  		svc.Spec.Selector = localSvc.Spec.Selector
   238  		svc.Spec.Ports = localSvc.Spec.Ports
   239  
   240  		// Use the local Service on creation as reference to determine the headlessness
   241  		// if the ServiceImport is not yet created. This allow to save a potential switch
   242  		// from non headless to headless (which involved a deletion + recreation)
   243  		// if there is no export conflict.
   244  		if svcImport == nil && !svcExists && localSvc.Spec.ClusterIP == corev1.ClusterIPNone {
   245  			svc.Spec.ClusterIP = corev1.ClusterIPNone
   246  		}
   247  	}
   248  
   249  	if svcImport != nil {
   250  		addOwnerReference(svc, svcImport)
   251  		svc.Spec.Ports = servicePorts(svcImport)
   252  		maps.Copy(svc.Annotations, svcImport.Annotations)
   253  		maps.Copy(svc.Labels, svcImport.Labels)
   254  	}
   255  
   256  	svc.Annotations[annotation.GlobalService] = "true"
   257  	svc.Annotations[annotation.SharedService] = "false"
   258  
   259  	svc.Labels[mcsapiv1alpha1.LabelServiceName] = req.NamespacedName.Name
   260  	// We set the source cluster label on the service as well so that the
   261  	// EndpointSlices created by kube-controller-manager will also mirror that label.
   262  	svc.Labels[mcsapiv1alpha1.LabelSourceCluster] = r.clusterName
   263  
   264  	if svcExport != nil {
   265  		addOwnerReference(svc, svcExport)
   266  		svc.Annotations[annotation.SharedService] = "true"
   267  	}
   268  
   269  	if !svcExists {
   270  		if err := r.Client.Create(ctx, svc); err != nil {
   271  			return controllerruntime.Fail(err)
   272  		}
   273  	} else {
   274  		if err := r.Client.Update(ctx, svc); err != nil {
   275  			return controllerruntime.Fail(err)
   276  		}
   277  	}
   278  
   279  	// Update the derived Service annotation on the ServiceImport object
   280  	// only after that the derived Service has been created for higher consistency.
   281  	return controllerruntime.Fail(r.addServiceImportDerivedAnnotation(ctx, svcImport, derivedServiceName))
   282  }
   283  
   284  // SetupWithManager sets up the controller with the Manager.
   285  func (r *mcsAPIServiceReconciler) SetupWithManager(mgr ctrl.Manager) error {
   286  	return ctrl.NewControllerManagedBy(mgr).
   287  		// Technically this controller owns the derived Services rather than the ServiceImports.
   288  		// However we operate on the ServiceImport (and ServiceExport) name rather than
   289  		// the derived service name so we say that we own ServiceImport here
   290  		// and always derive the name in the Reconcile function anyway.
   291  		For(&mcsapiv1alpha1.ServiceImport{}).
   292  		// Watch for changes to ServiceExport
   293  		Watches(&mcsapiv1alpha1.ServiceExport{}, &handler.EnqueueRequestForObject{}).
   294  		// Watch for changes to Services
   295  		Watches(&corev1.Service{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []ctrl.Request {
   296  			mcsAPIOwner := getMCSAPIOwner(obj.GetOwnerReferences())
   297  			if mcsAPIOwner == "" {
   298  				return []ctrl.Request{{NamespacedName: types.NamespacedName{
   299  					Name: obj.GetName(), Namespace: obj.GetNamespace(),
   300  				}}}
   301  			}
   302  			return []ctrl.Request{{NamespacedName: types.NamespacedName{
   303  				Name: mcsAPIOwner, Namespace: obj.GetNamespace(),
   304  			}}}
   305  		})).
   306  		Complete(r)
   307  }