• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dguerri / k8s-ssh-gateway-controller / 28707253456

04 Jul 2026 01:09PM UTC coverage: 90.2% (-0.5%) from 90.655%
28707253456

push

github

dguerri
fix: check Gateway ownership before validating routes

Add extractParentGatewayRef to identify a route's parent Gateway from its
first ParentRef before running strict validation. Reconcilers now determine
whether a route targets a Gateway managed by this controller upfront, so
routes owned by other controllers—which legitimately omit the optional
sectionName—no longer surface as spurious "Reconciler error" log spam.

Routes that are already owned or that target a managed Gateway continue
through full validation as before.

75 of 85 new or added lines in 4 files covered. (88.24%)

6 existing lines in 3 files now uncovered.

2034 of 2255 relevant lines covered (90.2%)

9.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.6
/controllers/httproute_controller.go
1
package controllers
2

3
import (
4
        "context"
5
        "errors"
6
        "fmt"
7
        "log/slog"
8

9
        "k8s.io/apimachinery/pkg/runtime"
10
        ctrl "sigs.k8s.io/controller-runtime"
11
        "sigs.k8s.io/controller-runtime/pkg/client"
12
        gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
13
)
14

15
type HTTPRouteReconciler struct {
16
        client.Client
17
        Scheme *runtime.Scheme
18

19
        GatewayReconciler *GatewayReconciler
20
}
21

22
func (r *HTTPRouteReconciler) SetupWithManager(mgr ctrl.Manager) error {
×
23
        return ctrl.NewControllerManagedBy(mgr).
×
24
                For(&gatewayv1.HTTPRoute{}).
×
25
                Complete(r)
×
26
}
×
27

28
func (r *HTTPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
15✔
29
        slog.With("function", "Reconcile", "httpRoute", req.NamespacedName).Debug("starting reconciliation")
15✔
30

15✔
31
        var k8sRoute gatewayv1.HTTPRoute
15✔
32
        if err := r.Get(ctx, req.NamespacedName, &k8sRoute); err != nil {
16✔
33
                slog.With("function", "Reconcile").Debug("unable to retrieve HTTPRoute", "http route", req.NamespacedName)
1✔
34
                return ctrl.Result{}, client.IgnoreNotFound(err)
1✔
35
        }
1✔
36

37
        isManagedByUs := containsString(k8sRoute.Finalizers, getHTTPRouteFinalizer())
14✔
38

14✔
39
        // Identify the parent Gateway from the route's first ParentRef so we can
14✔
40
        // decide whether this route is ours BEFORE running strict validation. Routes
14✔
41
        // belonging to other controllers legitimately omit the optional sectionName,
14✔
42
        // so validating them fully here would surface spurious reconcile errors.
14✔
43
        if !isManagedByUs {
25✔
44
                gwNamespace, gwName, err := extractParentGatewayRef(k8sRoute.Spec.ParentRefs, k8sRoute.Namespace)
11✔
45
                if err != nil {
12✔
46
                        slog.With("function", "Reconcile", "httpRoute", req.NamespacedName).Debug("route has no usable parent Gateway ref, skipping", "error", err)
1✔
47
                        return ctrl.Result{}, nil
1✔
48
                }
1✔
49
                isManaged, err := IsGatewayManaged(ctx, r.Client, gwNamespace, gwName)
10✔
50
                if err != nil {
11✔
51
                        slog.With("function", "Reconcile", "httpRoute", req.NamespacedName).Error("failed to check if gateway is managed",
1✔
52
                                "gateway", fmt.Sprintf("%s/%s", gwNamespace, gwName),
1✔
53
                                "error", err)
1✔
54
                        return ctrl.Result{}, err
1✔
55
                }
1✔
56
                if !isManaged {
12✔
57
                        slog.With("function", "Reconcile", "httpRoute", req.NamespacedName).Debug("gateway not managed by this controller, skipping",
3✔
58
                                "gateway", fmt.Sprintf("%s/%s", gwNamespace, gwName))
3✔
59
                        return ctrl.Result{}, nil
3✔
60
                }
3✔
61
        }
62

63
        // The route is ours (already owned, or it targets a Gateway we manage):
64
        // validate it fully.
65
        routeDetails, err := extractHTTPRouteDetails(&k8sRoute)
9✔
66
        if err != nil {
10✔
67
                slog.With("function", "Reconcile", "httpRoute", req.NamespacedName).Error("failed to extract HTTPRoute details", "error", err)
1✔
68
                return ctrl.Result{}, err
1✔
69
        }
1✔
70

71
        if !k8sRoute.DeletionTimestamp.IsZero() {
11✔
72
                if isManagedByUs {
6✔
73
                        return r.handleDeletion(ctx, req, &k8sRoute, routeDetails)
3✔
74
                }
3✔
75
                // Not ours, skip
UNCOV
76
                slog.With("function", "Reconcile", "httpRoute", req.NamespacedName).Debug("route being deleted but not managed by us, skipping")
×
UNCOV
77
                return ctrl.Result{}, nil
×
78
        }
79

80
        return r.handleAddOrUpdate(ctx, req, &k8sRoute, routeDetails)
5✔
81
}
82

83
func (r *HTTPRouteReconciler) handleAddOrUpdate(ctx context.Context, req ctrl.Request, k8sRoute *gatewayv1.HTTPRoute, routeDetails *routeDetails) (ctrl.Result, error) {
5✔
84
        // Add or Update.
5✔
85
        slog.With("function", "Reconcile", "httpRoute", req.NamespacedName).Debug("adding or updating HTTPRoute")
5✔
86
        // Add a finalizer so we can correctly clean up the route when it's deleted
5✔
87
        if !containsString(k8sRoute.Finalizers, getHTTPRouteFinalizer()) {
10✔
88
                k8sRoute.Finalizers = append(k8sRoute.Finalizers, getHTTPRouteFinalizer())
5✔
89
                if err := r.Update(ctx, k8sRoute); err != nil {
6✔
90
                        slog.With("function", "Reconcile", "httpRoute", req.NamespacedName).Error("failed to add finalizer", "error", err)
1✔
91
                        return ctrl.Result{}, err
1✔
92
                }
1✔
93
        }
94

95
        err := r.GatewayReconciler.SetRoute(
4✔
96
                ctx,
4✔
97
                routeDetails.gwNamespace, routeDetails.gwName,
4✔
98
                routeDetails.listenerName,
4✔
99
                routeDetails.routeName, routeDetails.routeNamespace,
4✔
100
                routeDetails.backendHost, routeDetails.backendPort)
4✔
101
        if err != nil {
7✔
102
                var notReadyErr *ErrGatewayNotReady
3✔
103
                var notFoundErr *ErrGatewayNotFound
3✔
104
                if errors.As(err, &notReadyErr) || errors.As(err, &notFoundErr) {
5✔
105
                        slog.With("function", "Reconcile", "httpRoute", req.NamespacedName).Warn("gateway not ready or not found, will retry with backoff",
2✔
106
                                "gateway", fmt.Sprintf("%s/%s", routeDetails.gwNamespace, routeDetails.gwName),
2✔
107
                                "error", err.Error())
2✔
108
                        // Return error to trigger controller-runtime's exponential backoff
2✔
109
                        return ctrl.Result{}, err
2✔
110
                }
2✔
111
                slog.With("function", "Reconcile", "httpRoute", req.NamespacedName).Error("failed to set route", "error", err)
1✔
112
                return ctrl.Result{}, err
1✔
113
        }
114
        slog.With("function", "Reconcile", "httpRoute", req.NamespacedName).Debug("route set successfully")
1✔
115
        return ctrl.Result{RequeueAfter: routeReconcilePeriod}, nil
1✔
116
}
117

118
func (r *HTTPRouteReconciler) handleDeletion(ctx context.Context, req ctrl.Request, k8sRoute *gatewayv1.HTTPRoute, routeDetails *routeDetails) (ctrl.Result, error) {
3✔
119
        // Handle deletion
3✔
120
        slog.With("function", "Reconcile", "httpRoute", req.NamespacedName).Info("processing HTTPRoute deletion")
3✔
121
        if containsString(k8sRoute.Finalizers, getHTTPRouteFinalizer()) {
6✔
122
                err := r.GatewayReconciler.RemoveRoute(
3✔
123
                        ctx,
3✔
124
                        routeDetails.gwNamespace, routeDetails.gwName,
3✔
125
                        routeDetails.listenerName,
3✔
126
                        routeDetails.routeName, routeDetails.routeNamespace,
3✔
127
                        routeDetails.backendHost, routeDetails.backendPort)
3✔
128
                if err != nil {
5✔
129
                        var gwNotFoundErr *ErrGatewayNotFound
2✔
130
                        var routeNotFoundErr *ErrRouteNotFound
2✔
131
                        if !errors.As(err, &gwNotFoundErr) && !errors.As(err, &routeNotFoundErr) {
3✔
132
                                slog.With("function", "Reconcile", "httpRoute", req.NamespacedName).Error("failed to remove route", "error", err)
1✔
133
                                return ctrl.Result{}, err
1✔
134
                        }
1✔
135
                }
136
                // Gateway or route were deleted, no need to requeue
137

138
                k8sRoute.Finalizers = removeString(k8sRoute.Finalizers, getHTTPRouteFinalizer())
2✔
139
                if err := r.Update(ctx, k8sRoute); err != nil {
3✔
140
                        slog.With("function", "Reconcile", "httpRoute", req.NamespacedName).Error("failed to remove finalizer", "error", err)
1✔
141
                        return ctrl.Result{}, err
1✔
142
                }
1✔
143
                slog.With("function", "Reconcile", "httpRoute", req.NamespacedName).Debug("route removed successfully")
1✔
144
        }
145
        return ctrl.Result{RequeueAfter: routeReconcilePeriod}, nil
1✔
146
}
147

148
// extractHTTPRouteDetails extracts common route details from the resource.
149
func extractHTTPRouteDetails(k8sRoute *gatewayv1.HTTPRoute) (*routeDetails, error) {
18✔
150
        // Validate ParentRefs
18✔
151
        if len(k8sRoute.Spec.ParentRefs) < 1 {
19✔
152
                return nil, fmt.Errorf("HTTPRoute %s/%s must have at least one ParentRef", k8sRoute.Namespace, k8sRoute.Name)
1✔
153
        }
1✔
154
        if len(k8sRoute.Spec.ParentRefs) > 1 {
17✔
155
                slog.With("route", fmt.Sprintf("%s/%s", k8sRoute.Namespace, k8sRoute.Name)).
×
156
                        Debug("HTTPRoute has more than one ParentRef, only the first will be used")
×
157
        }
×
158
        parent := k8sRoute.Spec.ParentRefs[0]
17✔
159

17✔
160
        // Validate ParentRef.Name
17✔
161
        if parent.Name == "" {
18✔
162
                return nil, fmt.Errorf("ParentRef name is empty for HTTPRoute %s/%s", k8sRoute.Namespace, k8sRoute.Name)
1✔
163
        }
1✔
164

165
        // Default ParentRef.Namespace
166
        parentNamespace := k8sRoute.Namespace // Default to the HTTPRoute's namespace
16✔
167
        if parent.Namespace != nil {
16✔
168
                parentNamespace = string(*parent.Namespace)
×
169
        } else {
16✔
170
                slog.With("route", fmt.Sprintf("%s/%s", k8sRoute.Namespace, k8sRoute.Name)).
16✔
171
                        Debug("ParentRef namespace is nil, defaulting to HTTPRoute namespace")
16✔
172
        }
16✔
173

174
        // Validate ParentRef.SectionName
175
        if parent.SectionName == nil {
17✔
176
                return nil, fmt.Errorf("ParentRef sectionName is nil for HTTPRoute %s/%s", k8sRoute.Namespace, k8sRoute.Name)
1✔
177
        }
1✔
178

179
        // Validate Rules
180
        if len(k8sRoute.Spec.Rules) < 1 {
17✔
181
                return nil, fmt.Errorf("HTTPRoute %s/%s must have at least one Rule", k8sRoute.Namespace, k8sRoute.Name)
2✔
182
        }
2✔
183
        if len(k8sRoute.Spec.Rules) > 1 {
13✔
184
                slog.With("route", fmt.Sprintf("%s/%s", k8sRoute.Namespace, k8sRoute.Name)).
×
185
                        Debug("HTTPRoute has more than one Rule, only the first will be used")
×
186
        }
×
187
        rule := k8sRoute.Spec.Rules[0]
13✔
188

13✔
189
        // Validate BackendRefs
13✔
190
        if len(rule.BackendRefs) < 1 {
14✔
191
                return nil, fmt.Errorf("HTTPRoute %s/%s must have at least one BackendRef", k8sRoute.Namespace, k8sRoute.Name)
1✔
192
        }
1✔
193
        if len(rule.BackendRefs) > 1 {
12✔
194
                slog.With("route", fmt.Sprintf("%s/%s", k8sRoute.Namespace, k8sRoute.Name)).
×
195
                        Debug("HTTPRoute Rule has more than one BackendRef, only the first will be used")
×
196
        }
×
197
        k8Svc := rule.BackendRefs[0]
12✔
198

12✔
199
        // Validate BackendRef.Name
12✔
200
        if k8Svc.Name == "" {
13✔
201
                return nil, fmt.Errorf("BackendRef name is empty for HTTPRoute %s/%s", k8sRoute.Namespace, k8sRoute.Name)
1✔
202
        }
1✔
203

204
        // Validate BackendRef.Port
205
        if k8Svc.Port == nil {
12✔
206
                return nil, fmt.Errorf("BackendRef port is nil for HTTPRoute %s/%s", k8sRoute.Namespace, k8sRoute.Name)
1✔
207
        }
1✔
208

209
        // Validate HTTPRoute.Namespace
210
        if k8sRoute.Namespace == "" {
11✔
211
                return nil, fmt.Errorf("HTTPRoute namespace is nil or empty for HTTPRoute %s", k8sRoute.Name)
1✔
212
        }
1✔
213

214
        // Default BackendRef.Namespace
215
        backendNamespace := k8sRoute.Namespace // Default to the HTTPRoute's namespace
9✔
216
        if k8Svc.Namespace != nil {
9✔
217
                backendNamespace = string(*k8Svc.Namespace)
×
218
        } else {
9✔
219
                slog.With("route", fmt.Sprintf("%s/%s", k8sRoute.Namespace, k8sRoute.Name)).
9✔
220
                        Debug("BackendRef namespace is nil, defaulting to HTTPRoute namespace")
9✔
221
        }
9✔
222

223
        return &routeDetails{
9✔
224
                routeName:      string(k8sRoute.Name),
9✔
225
                routeNamespace: string(k8sRoute.Namespace),
9✔
226
                gwName:         string(parent.Name),
9✔
227
                gwNamespace:    parentNamespace,
9✔
228
                listenerName:   string(*parent.SectionName),
9✔
229
                backendHost:    getSvcHostname(string(k8Svc.Name), backendNamespace),
9✔
230
                backendPort:    int(*k8Svc.Port),
9✔
231
        }, nil
9✔
232

233
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc