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 }