Files
netbird-kubernetes-operator/internal/controller/httproute_controller.go
T
Philip LaineandGitHub 39ab189fae Share NBResource between different routes (#149)
This changes the behavior of the Netbird resource so that they can be
shared between routes rather than having one unique created per route.
This is important as we may want to have different route types pointing
to the same service.

Signed-off-by: Philip Laine <philip.laine@gmail.com>
2026-03-19 18:28:35 +01:00

296 lines
8.2 KiB
Go

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)
}