1313// a trigger annotation on each owning Gateway. Because the touch happens after
1414// the new spec is already in the shared cache, EG re-translates against fresh
1515// data and the mode change reaches the edge in seconds.
16+ //
17+ // Deletion and targetRef removal are the mirror image: when a TPP is deleted or
18+ // a Gateway is dropped from spec.targetRefs, the cache no longer programs any WAF
19+ // for that Gateway, but nothing forces EG to re-translate — so the edge keeps
20+ // serving the last (possibly Enforce, still-blocking) program. Standard
21+ // Reconcile receives only the object key on delete, not the last-known object,
22+ // so the controller records the owning Gateway set of each TPP in memory and, on
23+ // delete or a targetRef edit, clears the trigger annotation on every Gateway that
24+ // dropped out of the set. Removing the annotation is itself a Gateway change, so
25+ // EG re-translates against the now-empty cache and drops the orphaned WAF config.
1626
1727package retrigger
1828
1929import (
2030 "context"
2131 "fmt"
2232 "strconv"
33+ "sync"
2334
35+ "github.com/go-logr/logr"
2436 "k8s.io/apimachinery/pkg/types"
2537 ctrl "sigs.k8s.io/controller-runtime"
2638 "sigs.k8s.io/controller-runtime/pkg/builder"
@@ -42,9 +54,23 @@ const tppTriggerAnnotationPrefix = "networking.datumapis.com/tpp-generation-"
4254
4355// TPPReconciler touches the trigger annotation on every Gateway a
4456// TrafficProtectionPolicy targets whenever the policy's spec changes, forcing EG
45- // to re-translate and re-run the extension hook against the fresh cache.
57+ // to re-translate and re-run the extension hook against the fresh cache. On
58+ // delete or targetRef removal it clears the annotation on Gateways that dropped
59+ // out of the policy's target set so EG re-translates and drops the orphaned WAF.
4660type TPPReconciler struct {
4761 Client client.Client
62+
63+ // mu guards lastTargets. Reconciles for distinct TPPs run concurrently, so
64+ // the previous-target map needs its own lock (controller-runtime only
65+ // serialises reconciles that share a key).
66+ mu sync.Mutex
67+ // lastTargets records the owning Gateway set last stamped for each TPP,
68+ // keyed by namespaced name. It is the source for "which Gateways did this
69+ // TPP used to govern?" on a delete (where the object is gone) or a targetRef
70+ // edit (where only the current targets are otherwise visible). Populated by
71+ // the create event every live TPP fires on startup, so a restart re-learns
72+ // the set before any subsequent edit or delete.
73+ lastTargets map [types.NamespacedName ][]string
4874}
4975
5076// tppTriggerAnnotationKey returns the per-TPP trigger annotation key.
@@ -53,28 +79,36 @@ func tppTriggerAnnotationKey(tppName string) string {
5379}
5480
5581// Reconcile stamps the TPP's observed generation onto the trigger annotation of
56- // every Gateway it targets. A TPP, the Gateways it targets, and the HTTPProxies
57- // that generate them share a namespace, and the Gateway is named after the
58- // targetRef (Gateway targetRefs name the Gateway directly; HTTPRoute targetRefs
59- // share the HTTPProxy name, which is also the Gateway name), so the
60- // TPP→Gateway mapping is local and name-derived.
82+ // every Gateway it targets, and clears the annotation on every Gateway that
83+ // dropped out of its target set since the last reconcile (a removed targetRef,
84+ // or — when the TPP itself is gone — all of them). A TPP, the Gateways it
85+ // targets, and the HTTPProxies that generate them share a namespace, and the
86+ // Gateway is named after the targetRef (Gateway targetRefs name the Gateway
87+ // directly; HTTPRoute targetRefs share the HTTPProxy name, which is also the
88+ // Gateway name), so the TPP→Gateway mapping is local and name-derived.
6189//
62- // The Gateway patch is a merge patch with no preceding Get: if the generation is
63- // unchanged the API server treats it as a no-op (no resourceVersion bump, no EG
64- // event), so it is naturally idempotent and never triggers a spurious
65- // re-translation.
90+ // Each Gateway patch is a merge patch with no preceding Get: an unchanged
91+ // generation (or clearing an already-absent annotation) is a server-side no-op
92+ // (no resourceVersion bump, no EG event), so it is naturally idempotent and
93+ // never triggers a spurious re-translation.
6694func (r * TPPReconciler ) Reconcile (ctx context.Context , req ctrl.Request ) (ctrl.Result , error ) {
6795 logger := log .FromContext (ctx )
96+ annotationKey := tppTriggerAnnotationKey (req .Name )
6897
6998 var tpp networkingv1alpha.TrafficProtectionPolicy
7099 if err := r .Client .Get (ctx , req .NamespacedName , & tpp ); err != nil {
71- return ctrl.Result {}, client .IgnoreNotFound (err )
100+ if client .IgnoreNotFound (err ) != nil {
101+ return ctrl.Result {}, err
102+ }
103+ // Deleted: nudge every Gateway the policy used to govern so EG
104+ // re-translates against the now-empty cache and drops the orphaned WAF.
105+ return ctrl.Result {}, r .clearGateways (ctx , logger , req .NamespacedName , annotationKey , r .takeTargets (req .NamespacedName ), "delete" )
72106 }
73107
74108 value := strconv .FormatInt (tpp .Generation , 10 )
75- annotationKey := tppTriggerAnnotationKey (tpp .Name )
76109
77110 seen := make (map [string ]struct {}, len (tpp .Spec .TargetRefs ))
111+ current := make ([]string , 0 , len (tpp .Spec .TargetRefs ))
78112 var firstErr error
79113 for _ , ref := range tpp .Spec .TargetRefs {
80114 gwName := string (ref .Name )
@@ -85,6 +119,7 @@ func (r *TPPReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R
85119 continue
86120 }
87121 seen [gwName ] = struct {}{}
122+ current = append (current , gwName )
88123
89124 gwKey := client.ObjectKey {Namespace : tpp .Namespace , Name : gwName }
90125 if err := r .touchGateway (ctx , gwKey , annotationKey , value ); err != nil {
@@ -99,9 +134,37 @@ func (r *TPPReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R
99134 "gateway" , gwKey , "tpp" , tpp .Name , "generation" , value )
100135 }
101136
137+ // Clear Gateways that were targeted last time but are not now. On any error
138+ // touching a current target, keep the previous set so the dropped Gateways
139+ // are retried on requeue rather than forgotten.
140+ prev := r .swapTargets (req .NamespacedName , current , firstErr == nil )
141+ if err := r .clearGateways (ctx , logger , req .NamespacedName , annotationKey , removedTargets (prev , seen ), "targetRef removal" ); err != nil && firstErr == nil {
142+ firstErr = err
143+ }
144+
102145 return ctrl.Result {}, firstErr
103146}
104147
148+ // clearGateways removes the trigger annotation from each named Gateway in the
149+ // policy's namespace, returning the first error encountered.
150+ func (r * TPPReconciler ) clearGateways (ctx context.Context , logger logr.Logger , tpp types.NamespacedName , annotationKey string , gwNames []string , reason string ) error {
151+ var firstErr error
152+ for _ , gwName := range gwNames {
153+ gwKey := client.ObjectKey {Namespace : tpp .Namespace , Name : gwName }
154+ if err := r .clearGateway (ctx , gwKey , annotationKey ); err != nil {
155+ logger .Error (err , "failed to clear gateway trigger for orphaned TPP target" ,
156+ "gateway" , gwKey , "tpp" , tpp .Name , "reason" , reason )
157+ if firstErr == nil {
158+ firstErr = err
159+ }
160+ continue
161+ }
162+ logger .Info ("cleared gateway trigger to drop orphaned WAF config" ,
163+ "gateway" , gwKey , "tpp" , tpp .Name , "reason" , reason )
164+ }
165+ return firstErr
166+ }
167+
105168// touchGateway applies a merge patch setting the trigger annotation. A
106169// non-existent Gateway is ignored: EG translates a Gateway when it is created,
107170// reading the (already fresh) extension cache, so there is nothing to nudge yet.
@@ -117,9 +180,68 @@ func (r *TPPReconciler) touchGateway(ctx context.Context, key client.ObjectKey,
117180 return nil
118181}
119182
183+ // clearGateway removes the trigger annotation via a merge patch (JSON null
184+ // deletes the key). Removing the annotation is a Gateway change, so EG
185+ // re-translates; clearing an already-absent key is a server-side no-op.
186+ func (r * TPPReconciler ) clearGateway (ctx context.Context , key client.ObjectKey , annotationKey string ) error {
187+ gw := & gatewayv1.Gateway {}
188+ gw .Namespace = key .Namespace
189+ gw .Name = key .Name
190+
191+ body := fmt .Appendf (nil , `{"metadata":{"annotations":{%q:null}}}` , annotationKey )
192+ if err := r .Client .Patch (ctx , gw , client .RawPatch (types .MergePatchType , body )); err != nil {
193+ return client .IgnoreNotFound (err )
194+ }
195+ return nil
196+ }
197+
198+ // swapTargets records the TPP's current owning Gateway set and returns the set
199+ // from the previous reconcile. When commit is false the previous set is left in
200+ // place (a touch failed, so the dropped Gateways must stay pending for retry)
201+ // but still returned for diffing.
202+ func (r * TPPReconciler ) swapTargets (key types.NamespacedName , current []string , commit bool ) []string {
203+ r .mu .Lock ()
204+ defer r .mu .Unlock ()
205+ prev := r .lastTargets [key ]
206+ if ! commit {
207+ return prev
208+ }
209+ if r .lastTargets == nil {
210+ r .lastTargets = make (map [types.NamespacedName ][]string )
211+ }
212+ if len (current ) == 0 {
213+ delete (r .lastTargets , key )
214+ } else {
215+ r .lastTargets [key ] = current
216+ }
217+ return prev
218+ }
219+
220+ // takeTargets returns and forgets the TPP's recorded owning Gateway set (used on
221+ // delete, where there is no current set to keep).
222+ func (r * TPPReconciler ) takeTargets (key types.NamespacedName ) []string {
223+ r .mu .Lock ()
224+ defer r .mu .Unlock ()
225+ prev := r .lastTargets [key ]
226+ delete (r .lastTargets , key )
227+ return prev
228+ }
229+
230+ // removedTargets returns the members of prev not present in current.
231+ func removedTargets (prev []string , current map [string ]struct {}) []string {
232+ var removed []string
233+ for _ , name := range prev {
234+ if _ , ok := current [name ]; ! ok {
235+ removed = append (removed , name )
236+ }
237+ }
238+ return removed
239+ }
240+
120241// SetupWithManager registers the controller. It reconciles a TPP only when its
121- // spec changes (generation bump) — the extension server reads spec fields only
122- // (mode, targetRefs, ruleSets, sampling), so status/metadata churn is ignored.
242+ // spec changes (generation bump) or it is deleted — the extension server reads
243+ // spec fields only (mode, targetRefs, ruleSets, sampling), so status/metadata
244+ // churn is ignored.
123245func (r * TPPReconciler ) SetupWithManager (mgr ctrl.Manager ) error {
124246 return ctrl .NewControllerManagedBy (mgr ).
125247 For (& networkingv1alpha.TrafficProtectionPolicy {}, builder .WithPredicates (tppSpecChangedPredicate ())).
@@ -128,15 +250,14 @@ func (r *TPPReconciler) SetupWithManager(mgr ctrl.Manager) error {
128250}
129251
130252// tppSpecChangedPredicate admits creates (so TPPs already present when the
131- // controller starts stamp their Gateways once) and updates that bump the
132- // generation (any spec change: mode flip, targetRef edit, ruleset/sampling
133- // change). Deletes are not handled here: a deleted TPP leaves no object to read
134- // targetRefs from, and its Gateways are not re-translated by this arm — see the
135- // known-limitation note in the PR.
253+ // controller starts stamp their Gateways, and their target set is learned before
254+ // any later edit or delete), updates that bump the generation (any spec change:
255+ // mode flip, targetRef edit, ruleset/sampling change), and deletes (so a removed
256+ // policy's Gateways are re-translated against the now-empty cache).
136257func tppSpecChangedPredicate () predicate.Predicate {
137258 return predicate.Funcs {
138259 CreateFunc : func (event.CreateEvent ) bool { return true },
139- DeleteFunc : func (event.DeleteEvent ) bool { return false },
260+ DeleteFunc : func (event.DeleteEvent ) bool { return true },
140261 UpdateFunc : func (e event.UpdateEvent ) bool {
141262 oldT , ok1 := e .ObjectOld .(* networkingv1alpha.TrafficProtectionPolicy )
142263 newT , ok2 := e .ObjectNew .(* networkingv1alpha.TrafficProtectionPolicy )
0 commit comments