package controller import ( "context" "fmt" "time" netbird "github.com/netbirdio/netbird/shared/management/client/rest" "github.com/netbirdio/netbird/shared/management/http/api" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/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/controller/controllerutil" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1" "github.com/netbirdio/kubernetes-operator/internal/util" ) const ( HTTPRouteFinalizer = "gateway.netbird.io/httproute" ) type HTTPRouteReconciler struct { client.Client Netbird *netbird.Client ClusterDNS string } // nolint:gocyclo func (r *HTTPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := ctrl.Log.WithName("HTTPRoute").WithValues("namespace", req.Namespace, "name", req.Name) hr := gatewayv1.HTTPRoute{} err := r.Get(ctx, req.NamespacedName, &hr) if err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } if !hr.DeletionTimestamp.IsZero() { return r.reconcileDelete(ctx, hr) } for _, parent := range hr.Spec.ParentRefs { // Check if controller is responsible for route. parentNamespace := hr.Namespace if parent.Namespace != nil { parentNamespace = string(*parent.Namespace) } gw := &gatewayv1.Gateway{} err = r.Client.Get(ctx, types.NamespacedName{Namespace: parentNamespace, Name: string(parent.Name)}, gw) if err != nil { return ctrl.Result{}, err } gwc := &gatewayv1.GatewayClass{} err := r.Get(ctx, client.ObjectKey{Name: string(gw.Spec.GatewayClassName)}, gwc) if err != nil { return ctrl.Result{}, err } if gwc.Spec.ControllerName != GatewayControllerName { continue } if !meta.IsStatusConditionTrue(gw.Status.Conditions, string(gatewayv1.GatewayConditionProgrammed)) { logger.Info("gateway is not ready", "name", gw.ObjectMeta.Name) return ctrl.Result{RequeueAfter: 1 * time.Second}, nil } nbrp := &netbirdiov1.NBRoutingPeer{} err = r.Get(ctx, types.NamespacedName{Namespace: gw.Namespace, Name: gw.Spec.Infrastructure.ParametersRef.Name}, nbrp) if err != nil { return ctrl.Result{}, err } if controllerutil.AddFinalizer(&hr, HTTPRouteFinalizer) { err = r.Client.Update(ctx, &hr) if err != nil { return ctrl.Result{}, err } } // Create network resources. svcIdx := map[string]corev1.Service{} for _, rule := range hr.Spec.Rules { for _, ref := range rule.BackendRefs { // TODO (phillebaba): Support reference grants. key := client.ObjectKey{Namespace: hr.Namespace, Name: string(ref.Name)} var svc corev1.Service err := r.Client.Get(ctx, key, &svc) if err != nil { return ctrl.Result{}, err } svcIdx[svc.Name] = svc } } for _, svc := range svcIdx { nbResource := netbirdiov1.NBResource{ ObjectMeta: metav1.ObjectMeta{ Name: svc.Name, Namespace: svc.Namespace, }, } _, err := controllerutil.CreateOrUpdate(ctx, r.Client, &nbResource, func() error { err = controllerutil.SetControllerReference(&svc, &nbResource, r.Scheme(), controllerutil.WithBlockOwnerDeletion(false)) if err != nil { return err } err = controllerutil.SetOwnerReference(&hr, &nbResource, r.Scheme()) if err != nil { return err } nbResource.Spec = netbirdiov1.NBResourceSpec{ Name: svc.Name, NetworkID: *nbrp.Status.NetworkID, Address: fmt.Sprintf("%s.%s.%s", svc.Name, svc.Namespace, r.ClusterDNS), Groups: []string{}, } return nil }) if err != nil { return ctrl.Result{}, err } } targets := []api.ServiceTarget{} for _, svc := range svcIdx { var nbResource netbirdiov1.NBResource err := r.Client.Get(ctx, client.ObjectKeyFromObject(&svc), &nbResource) if err != nil { return ctrl.Result{}, err } ready := func() bool { for _, cond := range nbResource.Status.Conditions { if cond.Type == netbirdiov1.NBSetupKeyReady && cond.Status == corev1.ConditionTrue { return true } } return false }() if !ready { return ctrl.Result{RequeueAfter: 1 * time.Second}, nil } target := api.ServiceTarget{ Enabled: true, Path: nil, TargetId: *nbResource.Status.NetworkResourceID, Protocol: "http", TargetType: "domain", } targets = append(targets, target) } // Create proxy service. proxyServices, err := r.Netbird.ReverseProxyServices.List(ctx) if err != nil { return ctrl.Result{}, err } for _, hostname := range hr.Spec.Hostnames { proxyReq := api.PostApiReverseProxiesServicesJSONRequestBody{ Auth: api.ServiceAuthConfig{}, Domain: string(hostname), Enabled: true, Name: string(hostname), PassHostHeader: util.Ptr(false), RewriteRedirects: util.Ptr(false), Targets: targets, } err := func() error { for _, proxyService := range proxyServices { if proxyService.Domain != string(hostname) { continue } _, err := r.Netbird.ReverseProxyServices.Update(ctx, proxyService.Id, proxyReq) if err != nil { return err } } _, err := r.Netbird.ReverseProxyServices.Create(ctx, proxyReq) if err != nil { return err } return nil }() if err != nil { return ctrl.Result{}, err } } } return ctrl.Result{}, nil } func (r *HTTPRouteReconciler) reconcileDelete(ctx context.Context, hr gatewayv1.HTTPRoute) (ctrl.Result, error) { // Index all proxy services. proxyServices, err := r.Netbird.ReverseProxyServices.List(ctx) if err != nil { return ctrl.Result{}, err } proxyIdx := map[string]string{} for _, proxyService := range proxyServices { proxyIdx[proxyService.Domain] = proxyService.Id } for _, parent := range hr.Spec.ParentRefs { parentNamespace := hr.Namespace if parent.Namespace != nil { parentNamespace = string(*parent.Namespace) } gw := &gatewayv1.Gateway{} err := r.Client.Get(ctx, types.NamespacedName{Namespace: parentNamespace, Name: string(parent.Name)}, gw) if err != nil { return ctrl.Result{}, err } gwc := &gatewayv1.GatewayClass{} err = r.Get(ctx, client.ObjectKey{Name: string(gw.Spec.GatewayClassName)}, gwc) if err != nil { return ctrl.Result{}, err } if gwc.Spec.ControllerName != GatewayControllerName { continue } // Remove the resource from the resource. svcIdx := map[string]corev1.Service{} for _, rule := range hr.Spec.Rules { for _, ref := range rule.BackendRefs { // TODO (phillebaba): Support reference grants. key := client.ObjectKey{Namespace: hr.Namespace, Name: string(ref.Name)} var svc corev1.Service err := r.Client.Get(ctx, key, &svc) if err != nil { return ctrl.Result{}, err } svcIdx[svc.Name] = svc } } for _, svc := range svcIdx { var nbResource netbirdiov1.NBResource err = r.Client.Get(ctx, client.ObjectKeyFromObject(&svc), &nbResource) if err != nil { return ctrl.Result{}, err } err = controllerutil.RemoveOwnerReference(&hr, &nbResource, r.Scheme()) if err != nil { return ctrl.Result{}, err } if len(nbResource.OwnerReferences) > 1 { err = r.Client.Update(ctx, &nbResource) if err != nil { return ctrl.Result{}, err } } else { // TODO: Precondition that nothing has changed. err := r.Client.Delete(ctx, &nbResource) if err != nil { return ctrl.Result{}, err } } } // Remove the target from the proxy service. for _, hostname := range hr.Spec.Hostnames { id, ok := proxyIdx[string(hostname)] if !ok { continue } err = r.Netbird.ReverseProxyServices.Delete(ctx, id) if err != nil && !netbird.IsNotFound(err) { return ctrl.Result{}, err } } } if controllerutil.RemoveFinalizer(&hr, HTTPRouteFinalizer) { err := r.Client.Update(ctx, &hr) if err != nil { return ctrl.Result{}, err } } return ctrl.Result{}, nil } // SetupWithManager sets up the controller with the Manager. func (r *HTTPRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&gatewayv1.HTTPRoute{}). Complete(r) }