Files
netbird-kubernetes-operator/internal/controller/httproute_controller.go
T
Philip LaineandGitHub 7acd175882 Gateway API support (#117)
This change adds support for the new proxy service to the operator
through Gateway API. This change attempts to standardize concepts around
the Gateway API to allow for compatibility with other projects.

Fixes #111
Fixes #44

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

287 lines
8.1 KiB
Go

package controller
import (
"context"
"encoding/json"
"fmt"
"strings"
netbird "github.com/netbirdio/netbird/shared/management/client/rest"
"github.com/netbirdio/netbird/shared/management/http/api"
"k8s.io/apimachinery/pkg/api/meta"
"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"
ResourceIDAnnotationKey = "gateway.netbird.io/resource-ids"
ProxyIDAnnotationKey = "gateway.netbird.io/proxy-ids"
)
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{Requeue: true}, 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.
oldResourceIDs := map[string]string{}
if s, ok := hr.Annotations[ResourceIDAnnotationKey]; ok {
err := json.Unmarshal([]byte(s), &oldResourceIDs)
if err != nil {
return ctrl.Result{}, err
}
}
resourceIDs := map[string]string{}
targets := []api.ServiceTarget{}
for _, rule := range hr.Spec.Rules {
for _, ref := range rule.BackendRefs {
// TODO (phillebaba): Support reference grants.
refNamespace := hr.Namespace
key := strings.Join([]string{string(ref.Name), refNamespace}, "/")
networkResourceReq := api.NetworkResourceRequest{
Name: fmt.Sprintf("%s/%s/%s/%s", refNamespace, gw.Name, hr.Name, ref.Name),
Enabled: true,
Address: fmt.Sprintf("%s.%s.%s", ref.Name, refNamespace, r.ClusterDNS),
Groups: []string{},
}
id, err := func() (string, error) {
if id, ok := oldResourceIDs[key]; ok {
_, err := r.Netbird.Networks.Resources(*nbrp.Status.NetworkID).Get(ctx, id)
if err != nil && !netbird.IsNotFound(err) {
return "", err
}
if err == nil {
_, err = r.Netbird.Networks.Resources(*nbrp.Status.NetworkID).Update(ctx, id, networkResourceReq)
if err != nil {
return "", err
}
delete(oldResourceIDs, key)
return id, nil
}
}
resource, err := r.Netbird.Networks.Resources(*nbrp.Status.NetworkID).Create(ctx, networkResourceReq)
if err != nil {
return "", err
}
return resource.Id, nil
}()
if err != nil {
return ctrl.Result{}, err
}
resourceIDs[key] = id
target := api.ServiceTarget{
Enabled: true,
Path: nil,
TargetId: id,
Protocol: "http",
TargetType: "domain",
}
targets = append(targets, target)
}
}
// Create proxy service.
oldProxyIDs := map[string]string{}
if s, ok := hr.Annotations[ProxyIDAnnotationKey]; ok {
err := json.Unmarshal([]byte(s), &oldProxyIDs)
if err != nil {
return ctrl.Result{}, err
}
}
proxyIDs := map[string]string{}
for _, hostname := range hr.Spec.Hostnames {
proxyCreate := api.PostApiReverseProxiesServicesJSONRequestBody{
Auth: api.ServiceAuthConfig{},
Domain: string(hostname),
Enabled: true,
Name: string(hostname),
PassHostHeader: util.Ptr(false),
RewriteRedirects: util.Ptr(false),
Targets: targets,
}
id, err := func() (string, error) {
if id, ok := oldProxyIDs[string(hostname)]; ok {
_, err := r.Netbird.ReverseProxyServices.Get(ctx, id)
if err != nil && !netbird.IsNotFound(err) {
return "", err
}
if err == nil {
_, err := r.Netbird.ReverseProxyServices.Update(ctx, id, proxyCreate)
if err != nil {
return "", nil
}
delete(oldProxyIDs, string(hostname))
return id, nil
}
}
proxy, err := r.Netbird.ReverseProxyServices.Create(ctx, proxyCreate)
if err != nil {
return "", err
}
return proxy.Id, nil
}()
if err != nil {
return ctrl.Result{}, err
}
proxyIDs[string(hostname)] = id
}
for _, id := range oldResourceIDs {
err = r.Netbird.Networks.Resources(*nbrp.Status.NetworkID).Delete(ctx, id)
if err != nil && !netbird.IsNotFound(err) {
return ctrl.Result{}, err
}
}
for _, id := range oldProxyIDs {
err = r.Netbird.ReverseProxyServices.Delete(ctx, id)
if err != nil && !netbird.IsNotFound(err) {
return ctrl.Result{}, err
}
}
b, err := json.Marshal(resourceIDs)
if err != nil {
return ctrl.Result{}, err
}
hr.Annotations[ResourceIDAnnotationKey] = string(b)
b, err = json.Marshal(proxyIDs)
if err != nil {
return ctrl.Result{}, err
}
hr.Annotations[ProxyIDAnnotationKey] = string(b)
err = r.Client.Update(ctx, &hr)
if err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
func (r *HTTPRouteReconciler) reconcileDelete(ctx context.Context, hr gatewayv1.HTTPRoute) (ctrl.Result, error) {
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
}
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
}
proxyIDs := map[string]string{}
if s, ok := hr.Annotations[ProxyIDAnnotationKey]; ok {
err := json.Unmarshal([]byte(s), &proxyIDs)
if err != nil {
return ctrl.Result{}, err
}
}
for _, id := range proxyIDs {
err = r.Netbird.ReverseProxyServices.Delete(ctx, id)
if err != nil && !netbird.IsNotFound(err) {
return ctrl.Result{}, err
}
}
resourceIDs := map[string]string{}
if s, ok := hr.Annotations[ResourceIDAnnotationKey]; ok {
err := json.Unmarshal([]byte(s), &resourceIDs)
if err != nil {
return ctrl.Result{}, err
}
}
for _, id := range resourceIDs {
err = r.Netbird.Networks.Resources(*nbrp.Status.NetworkID).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)
}