Skip to content
28 changes: 26 additions & 2 deletions internal/adc/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,12 @@ func (c *Client) applyStoreChanges(args Task, isDelete bool) (StoreDelta, error)
delta.Applied = args.Configs
}

for _, cfg := range delta.Deleted {
if err := c.Store.Delete(cfg.Name, args.ResourceTypes, args.Labels); err != nil {
for key, cfg := range delta.Deleted {
resourceTypes := args.ResourceTypes
if args.Key.Kind == types.KindGateway {
resourceTypes = gatewayEvictionTypes(resourceTypes, c.ConfigManager.HasResource(key, isGatewayProxyReferrer))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
if err := c.Store.Delete(cfg.Name, resourceTypes, args.Labels); err != nil {
c.log.Error(err, "store delete failed", "cfg", cfg, "args", args)
return StoreDelta{}, errors.Wrap(err, fmt.Sprintf("store delete failed for config %s", cfg.Name))
}
Expand All @@ -195,6 +199,26 @@ func (c *Client) applyStoreChanges(args Task, isDelete bool) (StoreDelta, error)
return delta, nil
}

// gatewayEvictionTypes evicts a Gateway's own global rule, stored under its labels, but
// keeps the GatewayProxy's plugin_metadata, shared by every Gateway and IngressClass
// referencing it, until the last of them is gone. Only the store is affected: the
// immediate sync keeps the caller's resource types, as ADC does not label-filter
// global rules.
func gatewayEvictionTypes(resourceTypes []string, referenced bool) []string {
resourceTypes = slices.DeleteFunc(slices.Clone(resourceTypes), func(t string) bool {
return t == adctypes.TypeGlobalRule || t == adctypes.TypePluginMetadata
})
resourceTypes = append(resourceTypes, adctypes.TypeGlobalRule)
if !referenced {
resourceTypes = append(resourceTypes, adctypes.TypePluginMetadata)
}
return resourceTypes
}

func isGatewayProxyReferrer(key types.NamespacedNameKind) bool {
return key.Kind == types.KindGateway || key.Kind == types.KindIngressClass
}

func (c *Client) applySync(ctx context.Context, args Task, delta StoreDelta) error {
c.syncMu.RLock()
defer c.syncMu.RUnlock()
Expand Down
12 changes: 5 additions & 7 deletions internal/controller/grpcroute_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ type GRPCRouteReconciler struct { //nolint:revive

Provider provider.Provider

parentResolver routeParentResolver

genericEvent chan event.GenericEvent

Updater status.Updater
Expand Down Expand Up @@ -94,7 +96,7 @@ func (r *GRPCRouteReconciler) SetupWithManager(mgr ctrl.Manager) error {
return false
},
DeleteFunc: func(e event.DeleteEvent) bool {
return false
return true
},
CreateFunc: func(e event.CreateEvent) bool {
return true
Expand Down Expand Up @@ -164,6 +166,7 @@ func (r *GRPCRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
gr := new(gatewayv1.GRPCRoute)
if err := r.Get(ctx, req.NamespacedName, gr); err != nil {
if client.IgnoreNotFound(err) == nil {
r.parentResolver.Forget(req.NamespacedName)
gr.Namespace = req.Namespace
gr.Name = req.Name

Expand Down Expand Up @@ -192,17 +195,12 @@ func (r *GRPCRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
msg: "Route is accepted",
}

gateways, unresolvedParents, err := ParseRouteParentRefs(ctx, r.Client, r.Log, gr, gr.Spec.ParentRefs)
gateways, err := r.parentResolver.Resolve(ctx, r.Client, r.Log, gr, gr.Spec.ParentRefs)
if err != nil {
return ctrl.Result{}, err
}

if len(gateways) == 0 {
if unresolvedParents {
// See the HTTPRoute reconciler: an unresolvable parentRef leaves ownership
// unknown rather than disproven, so the data plane must be left alone.
return ctrl.Result{}, nil
}
// See the HTTPRoute reconciler: a route that no longer references a
// Gateway managed by this controller must have its previously pushed
// configuration removed, or the data plane keeps serving it.
Expand Down
13 changes: 5 additions & 8 deletions internal/controller/httproute_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ type HTTPRouteReconciler struct { //nolint:revive

Provider provider.Provider

parentResolver routeParentResolver

genericEvent chan event.GenericEvent

Updater status.Updater
Expand Down Expand Up @@ -116,7 +118,7 @@ func (r *HTTPRouteReconciler) SetupWithManager(mgr ctrl.Manager) error {
return false
},
DeleteFunc: func(e event.DeleteEvent) bool {
return false
return true
},
CreateFunc: func(e event.CreateEvent) bool {
return true
Expand Down Expand Up @@ -162,6 +164,7 @@ func (r *HTTPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
hr := new(gatewayv1.HTTPRoute)
if err := r.Get(ctx, req.NamespacedName, hr); err != nil {
if client.IgnoreNotFound(err) == nil {
r.parentResolver.Forget(req.NamespacedName)
if err := r.updateHTTPRoutePolicyStatusOnDeleting(ctx, req.NamespacedName); err != nil {
r.Log.Error(err, "failed to update HTTPRoutePolicy status on HTTPRoute deleting", "httproute", req.NamespacedName)
}
Expand Down Expand Up @@ -193,18 +196,12 @@ func (r *HTTPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
msg: "Route is accepted",
}

gateways, unresolvedParents, err := ParseRouteParentRefs(ctx, r.Client, r.Log, hr, hr.Spec.ParentRefs)
gateways, err := r.parentResolver.Resolve(ctx, r.Client, r.Log, hr, hr.Spec.ParentRefs)
if err != nil {
return ctrl.Result{}, err
}

if len(gateways) == 0 {
if unresolvedParents {
// A missing Gateway or GatewayClass leaves ownership unknown rather than
// disproven. GatewayClass is cluster-scoped, so while one is absent every
// route under it resolves empty and deleting would drain the data plane.
return ctrl.Result{}, nil
}
// The route does not reference any Gateway managed by this controller.
// It may have referenced one before, e.g. when its parentRefs are
// repointed at a Gateway belonging to another GatewayClass, so the
Expand Down
10 changes: 3 additions & 7 deletions internal/controller/httproute_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,7 @@ type noopReadier struct {

func (noopReadier) Done(client.Object, k8stypes.NamespacedName) {}

// TestHTTPRouteReconcile_EmptyGateways covers the two ways a route resolves to no
// Gateway of this controller. Only a resolved parent naming another controller
// proves the route is not ours; an unresolvable parent must leave the data plane
// alone, since a missing cluster-scoped GatewayClass empties the list for every
// route under it at once.
// Routes with no known managed parents retract any stale configuration.
func TestHTTPRouteReconcile_EmptyGateways(t *testing.T) {
scheme := parentRefTestScheme(t)

Expand Down Expand Up @@ -85,9 +81,9 @@ func TestHTTPRouteReconcile_EmptyGateways(t *testing.T) {
wantDelete: true,
},
{
name: "gatewayclass missing",
name: "gatewayclass never resolved",
objects: []client.Object{route, gw("gone")},
wantDelete: false,
wantDelete: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
Expand Down
101 changes: 101 additions & 0 deletions internal/controller/route_parent_resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package controller

import (
"context"
"sync"

"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"

"github.com/apache/apisix-ingress-controller/internal/controller/config"
)

type resolvedParentClass struct {
gatewayUID types.UID
class *gatewayv1.GatewayClass
}

type routeParentOwnership struct {
uid types.UID
parents map[client.ObjectKey]resolvedParentClass
}

// routeParentResolver remembers ownership only for parents previously resolved
// for this route. A never-resolved sibling must not prevent deletion cleanup.
// Like the provider's desired configuration, this state is rebuilt on startup.
type routeParentResolver struct {
mu sync.Mutex
parents map[client.ObjectKey]routeParentOwnership
}

func (r *routeParentResolver) Forget(key client.ObjectKey) {
r.mu.Lock()
defer r.mu.Unlock()
delete(r.parents, key)
}

func (r *routeParentResolver) Resolve(ctx context.Context, cli client.Client, log logr.Logger, route client.Object, refs []gatewayv1.ParentReference) ([]RouteParentRefContext, error) {
// controller-runtime never reconciles one route concurrently, so the lock only
// guards the map, not the parent lookups between the read and the write.
routeKey := client.ObjectKeyFromObject(route)
r.mu.Lock()
previous := r.parents[routeKey].parents
if r.parents[routeKey].uid != route.GetUID() {
previous = nil
}
r.mu.Unlock()
next := make(map[client.ObjectKey]resolvedParentClass)
gateways, err := parseRouteParentRefs(ctx, cli, log, route, refs, func(gw *gatewayv1.Gateway) (*gatewayv1.GatewayClass, error) {
key := client.ObjectKeyFromObject(gw)
gc := &gatewayv1.GatewayClass{}
if err := cli.Get(ctx, client.ObjectKey{Name: string(gw.Spec.GatewayClassName)}, gc); err != nil {
if client.IgnoreNotFound(err) != nil {
return nil, err
}
known, ok := previous[key]
if !ok || known.gatewayUID != gw.UID || known.class.Name != string(gw.Spec.GatewayClassName) {
return nil, nil
}
// Keep resolving the surviving Gateway's current listeners without
// retaining configuration belonging to a different, deleted parent.
gc = known.class
}
if string(gc.Spec.ControllerName) == config.ControllerConfig.ControllerName {
next[key] = resolvedParentClass{gatewayUID: gw.UID, class: gc}
}
return gc, nil
})
if err != nil {
return nil, err
}
r.mu.Lock()
defer r.mu.Unlock()
if len(next) == 0 {
delete(r.parents, routeKey)
} else {
if r.parents == nil {
r.parents = make(map[client.ObjectKey]routeParentOwnership)
}
r.parents[routeKey] = routeParentOwnership{uid: route.GetUID(), parents: next}
}
return gateways, nil
}
Loading
Loading