package controller import ( "context" "fmt" cmv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" gw "github.com/example/gateway-cert-operator/internal/gateway" ) const ( // AnnotationGatewayName is the annotation key specifying which Gateway to update. AnnotationGatewayName = "gateway-cert-operator.io/gateway-name" // AnnotationGatewayNamespace optionally overrides the Gateway namespace. Defaults to Certificate namespace. AnnotationGatewayNamespace = "gateway-cert-operator.io/gateway-namespace" // AnnotationListenerPort optionally overrides the listener port. Defaults to 443. AnnotationListenerPort = "gateway-cert-operator.io/listener-port" // ListenerPrefix is prepended to managed listener names so we can distinguish ours. ListenerPrefix = "auto-" // IndexFieldGatewayTarget is the field index for mapping Certificates to their target Gateway. IndexFieldGatewayTarget = ".metadata.annotations.gatewayTarget" ) // CertificateReconciler reconciles Certificate objects and patches Gateway listeners. type CertificateReconciler struct { client.Client } // SetupCertificateReconciler creates the reconciler and registers watches. func SetupCertificateReconciler(mgr ctrl.Manager) error { r := &CertificateReconciler{Client: mgr.GetClient()} // Index Certificates by their target Gateway so we can list all certs for a given Gateway. if err := mgr.GetFieldIndexer().IndexField(context.Background(), &cmv1.Certificate{}, IndexFieldGatewayTarget, func(obj client.Object) []string { cert := obj.(*cmv1.Certificate) gwName, ok := cert.Annotations[AnnotationGatewayName] if !ok { return nil } gwNs := cert.Annotations[AnnotationGatewayNamespace] if gwNs == "" { gwNs = cert.Namespace } return []string{fmt.Sprintf("%s/%s", gwNs, gwName)} }); err != nil { return fmt.Errorf("failed to setup field index: %w", err) } return ctrl.NewControllerManagedBy(mgr). For(&cmv1.Certificate{}). // Secondary watch: if someone edits the Gateway, re-reconcile relevant Certificates. Watches(&gwapiv1.Gateway{}, handler.EnqueueRequestsFromMapFunc(r.gatewayToCertificates)). Complete(r) } // Reconcile handles Certificate create/update/delete events. func (r *CertificateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := log.FromContext(ctx) var cert cmv1.Certificate if err := r.Get(ctx, req.NamespacedName, &cert); err != nil { // Certificate deleted — we still need to reconcile the Gateway. // We can't know which Gateway it targeted anymore, so we rely on the // fact that the index query below will simply not return this cert, // and the Gateway will be patched without its listener. // To handle this properly, we reconcile ALL Gateways that had any // managed listener. In practice, deletion triggers the index update, // and the Gateway watch re-enqueues the right certs. return ctrl.Result{}, client.IgnoreNotFound(err) } gwName, ok := cert.Annotations[AnnotationGatewayName] if !ok { log.V(1).Info("certificate missing gateway annotation, skipping") return ctrl.Result{}, nil } gwNamespace := cert.Annotations[AnnotationGatewayNamespace] if gwNamespace == "" { gwNamespace = cert.Namespace } // List ALL certificates targeting this Gateway so we compute the full desired state. var certList cmv1.CertificateList if err := r.List(ctx, &certList, client.MatchingFields{ IndexFieldGatewayTarget: fmt.Sprintf("%s/%s", gwNamespace, gwName), }); err != nil { return ctrl.Result{}, fmt.Errorf("listing certificates for gateway %s/%s: %w", gwNamespace, gwName, err) } // Build desired listeners from all annotated certificates. desiredListeners := gw.BuildListenersFromCertificates(certList.Items, ListenerPrefix) // Fetch the target Gateway. var gateway gwapiv1.Gateway if err := r.Get(ctx, types.NamespacedName{Name: gwName, Namespace: gwNamespace}, &gateway); err != nil { return ctrl.Result{}, fmt.Errorf("getting gateway %s/%s: %w", gwNamespace, gwName, err) } // Merge: keep non-prefixed listeners, replace all auto-* listeners. updated := gw.MergeListeners(gateway.Spec.Listeners, desiredListeners, ListenerPrefix) if gw.ListenersEqual(gateway.Spec.Listeners, updated) { log.V(1).Info("gateway listeners already up to date") return ctrl.Result{}, nil } patch := client.MergeFrom(gateway.DeepCopy()) gateway.Spec.Listeners = updated if err := r.Patch(ctx, &gateway, patch); err != nil { return ctrl.Result{}, fmt.Errorf("patching gateway %s/%s: %w", gwNamespace, gwName, err) } log.Info("patched gateway listeners", "gateway", fmt.Sprintf("%s/%s", gwNamespace, gwName), "managedListeners", len(desiredListeners), ) return ctrl.Result{}, nil } // gatewayToCertificates maps a Gateway event back to all Certificates targeting it. func (r *CertificateReconciler) gatewayToCertificates(ctx context.Context, obj client.Object) []reconcile.Request { gw := obj.(*gwapiv1.Gateway) key := fmt.Sprintf("%s/%s", gw.Namespace, gw.Name) var certList cmv1.CertificateList if err := r.List(ctx, &certList, client.MatchingFields{ IndexFieldGatewayTarget: key, }); err != nil { log.FromContext(ctx).Error(err, "listing certificates for gateway", "gateway", key) return nil } requests := make([]reconcile.Request, 0, len(certList.Items)) for _, cert := range certList.Items { requests = append(requests, reconcile.Request{ NamespacedName: types.NamespacedName{ Name: cert.Name, Namespace: cert.Namespace, }, }) } return requests }