Skip to content

Commit 31521b5

Browse files
committed
🐛 prune stale HTTPRoutes when tags are removed
Signed-off-by: kahirokunn <[email protected]>
1 parent b658b58 commit 31521b5

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed

pkg/reconciler/ingress/ingress.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@ import (
2323

2424
apierrs "k8s.io/apimachinery/pkg/api/errors"
2525
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/apimachinery/pkg/labels"
27+
"k8s.io/apimachinery/pkg/util/sets"
2628

2729
"knative.dev/net-gateway-api/pkg/reconciler/ingress/config"
30+
"knative.dev/net-gateway-api/pkg/reconciler/ingress/resources"
2831
"knative.dev/net-gateway-api/pkg/status"
2932
"knative.dev/networking/pkg/apis/networking/v1alpha1"
3033
ingressreconciler "knative.dev/networking/pkg/client/injection/reconciler/networking/v1alpha1/ingress"
@@ -102,7 +105,10 @@ func (c *Reconciler) reconcileIngress(ctx context.Context, ing *v1alpha1.Ingress
102105

103106
routesReady := true
104107

108+
desiredRouteNames := sets.New[string]()
105109
for _, rule := range ing.Spec.Rules {
110+
desiredRouteNames.Insert(resources.LongestHost(rule.Hosts))
111+
106112
httproute, probeTargets, err := c.reconcileHTTPRoute(ctx, ingressHash, ing, &rule)
107113
if err != nil {
108114
return err
@@ -123,6 +129,26 @@ func (c *Reconciler) reconcileIngress(ctx context.Context, ing *v1alpha1.Ingress
123129
}
124130
}
125131

132+
// Delete HTTPRoutes that don't exist in the current Spec (i.e., tags removed and no longer referenced)
133+
{
134+
existingRoutes, err := c.httprouteLister.HTTPRoutes(ing.Namespace).List(labels.Everything())
135+
if err != nil {
136+
return fmt.Errorf("failed to list HTTPRoutes: %w", err)
137+
}
138+
for _, r := range existingRoutes {
139+
// Don't touch routes not owned by this Ingress
140+
if !metav1.IsControlledBy(r, ing) {
141+
continue
142+
}
143+
// Not in the desired set = unnecessary
144+
if !desiredRouteNames.Has(r.Name) {
145+
if err := c.gwapiclient.GatewayV1().HTTPRoutes(r.Namespace).Delete(ctx, r.Name, metav1.DeleteOptions{}); err != nil && !apierrs.IsNotFound(err) {
146+
return fmt.Errorf("failed to delete stale HTTPRoute %s/%s: %w", r.Namespace, r.Name, err)
147+
}
148+
}
149+
}
150+
}
151+
126152
externalIngressTLS := ing.GetIngressTLSForVisibility(v1alpha1.IngressVisibilityExternalIP)
127153
listeners := make([]*gatewayapi.Listener, 0, len(externalIngressTLS))
128154
for _, tls := range externalIngressTLS {

pkg/reconciler/ingress/ingress_test.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,17 @@ package ingress
1818

1919
import (
2020
"context"
21+
"errors"
2122
"fmt"
2223
"testing"
2324
"time"
2425

2526
corev1 "k8s.io/api/core/v1"
27+
apierrs "k8s.io/apimachinery/pkg/api/errors"
2628
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
"k8s.io/apimachinery/pkg/labels"
2730
"k8s.io/apimachinery/pkg/runtime"
31+
"k8s.io/apimachinery/pkg/runtime/schema"
2832
"k8s.io/apimachinery/pkg/types"
2933
clientgotesting "k8s.io/client-go/testing"
3034
"k8s.io/utils/ptr"
@@ -47,6 +51,7 @@ import (
4751

4852
gatewayapi "sigs.k8s.io/gateway-api/apis/v1"
4953
gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
54+
gatewaylisters "sigs.k8s.io/gateway-api/pkg/client/listers/apis/v1"
5055

5156
. "knative.dev/net-gateway-api/pkg/reconciler/testing"
5257
. "knative.dev/pkg/reconciler/testing"
@@ -196,6 +201,103 @@ func TestReconcile(t *testing.T) {
196201
httpRoute(t, ing(withBasicSpec, withGatewayAPIclass), httpRouteReady),
197202
}, servicesAndEndpoints...),
198203
// no extra update
204+
}, {
205+
Name: "prune stale HTTPRoute when rule removed",
206+
Key: "ns/name",
207+
Objects: append([]runtime.Object{
208+
ing(withBasicSpec, withGatewayAPIclass, withFinalizer, makeItReady),
209+
httpRoute(t, ing(withBasicSpec, withGatewayAPIclass), httpRouteReady),
210+
HTTPRoute{
211+
Name: "stale.example.com",
212+
Namespace: "ns",
213+
Hostname: "stale.example.com",
214+
}.Build(),
215+
}, servicesAndEndpoints...),
216+
WantDeletes: []clientgotesting.DeleteActionImpl{
217+
clientgotesting.NewDeleteAction(
218+
schema.GroupVersionResource{
219+
Group: "gateway.networking.k8s.io",
220+
Version: "v1",
221+
Resource: "httproutes",
222+
},
223+
"ns",
224+
"stale.example.com",
225+
),
226+
},
227+
}, {
228+
Name: "prune skips non-owned HTTPRoute",
229+
Key: "ns/name",
230+
Objects: append(func() []runtime.Object {
231+
route := HTTPRoute{
232+
Name: "stale.example.com",
233+
Namespace: "ns",
234+
Hostname: "stale.example.com",
235+
}.Build()
236+
// Remove owner so it is not controlled by this Ingress
237+
route.OwnerReferences = nil
238+
return []runtime.Object{
239+
ing(withBasicSpec, withGatewayAPIclass, withFinalizer, makeItReady),
240+
httpRoute(t, ing(withBasicSpec, withGatewayAPIclass), httpRouteReady),
241+
route,
242+
}
243+
}(), servicesAndEndpoints...),
244+
// No deletes expected
245+
WantDeletes: []clientgotesting.DeleteActionImpl{},
246+
}, {
247+
Name: "prune list error bubbles up",
248+
Key: "ns/name",
249+
Ctx: withHTTPRouteListError(),
250+
Objects: append([]runtime.Object{
251+
ing(withBasicSpec, withGatewayAPIclass, withFinalizer, makeItReady),
252+
}, servicesAndEndpoints...),
253+
WantCreates: []runtime.Object{
254+
httpRoute(t, ing(withBasicSpec, withGatewayAPIclass)),
255+
},
256+
WantStatusUpdates: []clientgotesting.UpdateActionImpl{{
257+
Object: ing(withBasicSpec, withGatewayAPIclass, withFinalizer, makeItReady, func(i *v1alpha1.Ingress) {
258+
i.Status.MarkIngressNotReady("ReconcileIngressFailed", "Ingress reconciliation failed")
259+
}),
260+
}},
261+
WantEvents: []string{
262+
Eventf(corev1.EventTypeNormal, "Created", "Created HTTPRoute \"example.com\""),
263+
Eventf(corev1.EventTypeWarning, "InternalError", `failed to list HTTPRoutes: forced list error`),
264+
},
265+
WantErr: true,
266+
}, {
267+
Name: "prune delete NotFound tolerated",
268+
Key: "ns/name",
269+
WithReactors: []clientgotesting.ReactionFunc{
270+
func(a clientgotesting.Action) (bool, runtime.Object, error) {
271+
if a.GetVerb() == "delete" && a.GetResource().Resource == "httproutes" {
272+
name := a.(clientgotesting.DeleteActionImpl).Name
273+
return true, nil, apierrs.NewNotFound(
274+
schema.GroupResource{Group: "gateway.networking.k8s.io", Resource: "httproutes"},
275+
name,
276+
)
277+
}
278+
return false, nil, nil
279+
},
280+
},
281+
Objects: append([]runtime.Object{
282+
ing(withBasicSpec, withGatewayAPIclass, withFinalizer, makeItReady),
283+
httpRoute(t, ing(withBasicSpec, withGatewayAPIclass), httpRouteReady),
284+
HTTPRoute{
285+
Name: "stale.example.com",
286+
Namespace: "ns",
287+
Hostname: "stale.example.com",
288+
}.Build(),
289+
}, servicesAndEndpoints...),
290+
WantDeletes: []clientgotesting.DeleteActionImpl{
291+
clientgotesting.NewDeleteAction(
292+
schema.GroupVersionResource{
293+
Group: "gateway.networking.k8s.io",
294+
Version: "v1",
295+
Resource: "httproutes",
296+
},
297+
"ns",
298+
"stale.example.com",
299+
),
300+
},
199301
}}
200302

201303
table.Test(t, MakeFactory(func(ctx context.Context, listers *Listers, cmw configmap.Watcher) controller.Reconciler {
@@ -214,6 +316,11 @@ func TestReconcile(t *testing.T) {
214316
},
215317
}
216318

319+
// Force HTTPRoute List error when test context requests it.
320+
if ctx.Value(httpRouteListErrorKey{}) == true {
321+
r.httprouteLister = errorWrapHTTPRouteLister{inner: listers.GetHTTPRouteLister()}
322+
}
323+
217324
ingr := ingressreconciler.NewReconciler(ctx, logging.FromContext(ctx), fakeingressclient.Get(ctx),
218325
listers.GetIngressLister(), controller.GetEventRecorder(ctx), r, gatewayAPIIngressClassName,
219326
controller.Options{
@@ -226,6 +333,38 @@ func TestReconcile(t *testing.T) {
226333
}))
227334
}
228335

336+
// --- helpers for forcing lister errors in specific tests ---
337+
338+
type httpRouteListErrorKey struct{}
339+
340+
func withHTTPRouteListError() context.Context {
341+
return context.WithValue(context.Background(), httpRouteListErrorKey{}, true)
342+
}
343+
344+
type errorWrapHTTPRouteNamespaceLister struct {
345+
inner gatewaylisters.HTTPRouteNamespaceLister
346+
}
347+
348+
func (e errorWrapHTTPRouteNamespaceLister) List(selector labels.Selector) ([]*gatewayapi.HTTPRoute, error) {
349+
return nil, errors.New("forced list error")
350+
}
351+
352+
func (e errorWrapHTTPRouteNamespaceLister) Get(name string) (*gatewayapi.HTTPRoute, error) {
353+
return e.inner.Get(name)
354+
}
355+
356+
type errorWrapHTTPRouteLister struct {
357+
inner gatewaylisters.HTTPRouteLister
358+
}
359+
360+
func (e errorWrapHTTPRouteLister) List(selector labels.Selector) ([]*gatewayapi.HTTPRoute, error) {
361+
return e.inner.List(selector)
362+
}
363+
364+
func (e errorWrapHTTPRouteLister) HTTPRoutes(namespace string) gatewaylisters.HTTPRouteNamespaceLister {
365+
return errorWrapHTTPRouteNamespaceLister{inner: e.inner.HTTPRoutes(namespace)}
366+
}
367+
229368
func TestReconcileTLS(t *testing.T) {
230369
// The gateway API annoyingly has a number of
231370
secretName := "name-WE-STICK-A-LONG-UID-HERE"

0 commit comments

Comments
 (0)