@@ -20,42 +20,20 @@ import (
2020 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
2121 "sigs.k8s.io/controller-runtime/pkg/handler"
2222 "sigs.k8s.io/controller-runtime/pkg/log"
23- gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
2423 mcbuilder "sigs.k8s.io/multicluster-runtime/pkg/builder"
2524 mchandler "sigs.k8s.io/multicluster-runtime/pkg/handler"
2625 mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager"
2726 "sigs.k8s.io/multicluster-runtime/pkg/multicluster"
2827 mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile"
2928
30- networkingv1alpha "go.datum.net/network-services-operator/api/v1alpha"
3129 networkingv1alpha1 "go.datum.net/network-services-operator/api/v1alpha1"
3230 "go.datum.net/network-services-operator/internal/config"
33- downstreamclient "go.datum.net/network-services-operator/internal/downstreamclient"
3431)
3532
36- // connectorReadyAnnotationKey is patched onto downstream Gateways when a
37- // Connector's Ready condition flips online↔offline. Envoy Gateway's
38- // AnnotationChangedPredicate on Gateway fires a full re-translation so the
39- // extension server can re-apply the correct routing config from its cache.
40- //
41- // This is the Mode-B (eppEmissionEnabled:false) replacement for the trigger
42- // that EnvoyPatchPolicy objects provided in Mode A: the EPP itself was the
43- // EG-watched resource; in Mode B we touch a Gateway annotation instead.
44- // Only used when r.DownstreamCluster is set and EPP emission is disabled.
45- const connectorReadyAnnotationKey = "networking.datumapis.com/connector-ready-generation"
46-
4733// ConnectorReconciler reconciles a Connector object
4834type ConnectorReconciler struct {
4935 mgr mcmanager.Manager
5036 Config config.NetworkServicesOperator
51-
52- // DownstreamCluster is the edge/infra cluster where Gateways are
53- // materialized and where Envoy Gateway runs. When non-nil and EPP
54- // emission is disabled (Mode B), the reconciler touches a trigger
55- // annotation on affected downstream Gateways whenever a Connector's
56- // Ready condition flips, causing EG to re-translate via its
57- // AnnotationChangedPredicate on the Gateway resource.
58- DownstreamCluster cluster.Cluster
5937}
6038
6139// +kubebuilder:rbac:groups=networking.datumapis.com,resources=connectors,verbs=get;list;watch;create;update;patch;delete
@@ -89,9 +67,6 @@ func (r *ConnectorReconciler) Reconcile(ctx context.Context, req mcreconcile.Req
8967 defer logger .Info ("reconcile complete" )
9068
9169 originalStatus := connector .Status .DeepCopy ()
92- // Capture the Ready condition BEFORE any mutations so we can detect a
93- // status→Ready flip at the end of this reconcile (Mode B only).
94- prevReadyCondition := apimeta .FindStatusCondition (originalStatus .Conditions , networkingv1alpha1 .ConnectorConditionReady )
9570
9671 readyCondition := apimeta .FindStatusCondition (connector .Status .Conditions , networkingv1alpha1 .ConnectorConditionReady )
9772 if readyCondition == nil {
@@ -192,137 +167,19 @@ func (r *ConnectorReconciler) Reconcile(ctx context.Context, req mcreconcile.Req
192167 }
193168 }
194169
195- // Mode B (extension server): when the Connector's Ready condition flips
196- // (Lease expiry → offline, Lease renewal → online), touch a trigger
197- // annotation on each affected downstream Gateway so EG's
198- // AnnotationChangedPredicate fires a full re-translation. This replaces
199- // the trigger that EPP objects provided in Mode A.
200- //
201- // The extensionManager.resources watch (TPP + Connector) already handles
202- // spec-change triggers via GenerationChangedPredicate; status-only
203- // transitions (this path) require the Gateway annotation touch because
204- // status writes do NOT increment metadata.generation.
205- if ! r .Config .Gateway .IsEPPEmissionEnabled () && r .DownstreamCluster != nil {
206- newReadyCondition := apimeta .FindStatusCondition (connector .Status .Conditions , networkingv1alpha1 .ConnectorConditionReady )
207- if connectorReadyStatusChanged (prevReadyCondition , newReadyCondition ) {
208- if annotationErr := r .touchDownstreamGatewayAnnotations (ctx , cl .GetClient (), string (req .ClusterName ), & connector ); annotationErr != nil {
209- // Log and continue — annotation touch is best-effort; a
210- // missed touch means Envoy holds last-known-good config
211- // until the next EG rebuild rather than crashing.
212- logger .Error (annotationErr , "failed to touch downstream gateway annotations for connector ready state transition" )
213- }
214- }
215- }
170+ // The downstream re-translation trigger for a Connector liveness flip lives
171+ // at the edge: the extension server's gateway re-translation controller
172+ // (internal/extensionserver/retrigger) watches the replicated Connector and
173+ // touches the owning Gateway annotation locally. Doing it here (project side)
174+ // raced the upstream-status annotation's independent Karmada hub→edge
175+ // propagation — EG could re-translate against a still-offline edge cache.
216176
217177 if leaseStatus .requeueAfter != nil {
218178 return ctrl.Result {RequeueAfter : * leaseStatus .requeueAfter }, nil
219179 }
220180 return ctrl.Result {}, nil
221181}
222182
223- // connectorReadyStatusChanged returns true when the Ready condition's Status
224- // has changed between the previous and current condition. Nil → non-nil (first
225- // time the condition is set) is also treated as a change.
226- func connectorReadyStatusChanged (prev , next * metav1.Condition ) bool {
227- if prev == nil && next == nil {
228- return false
229- }
230- if prev == nil || next == nil {
231- return true
232- }
233- return prev .Status != next .Status
234- }
235-
236- // connectorReadyAnnotationValue returns a deterministic string encoding of
237- // the Connector's Ready condition for use as the annotation value.
238- // The value changes only when the Ready Status changes (True ↔ False), so
239- // patching it onto a downstream Gateway is idempotent across reconciles that
240- // leave Ready in the same state.
241- func connectorReadyAnnotationValue (readyCondition * metav1.Condition ) string {
242- if readyCondition == nil {
243- return "Unknown/Unknown"
244- }
245- return fmt .Sprintf ("%s/%s" , readyCondition .Status , readyCondition .Reason )
246- }
247-
248- // touchDownstreamGatewayAnnotations patches a trigger annotation on every
249- // downstream Gateway backed by an HTTPProxy that references this Connector.
250- // The annotation change fires EG's AnnotationChangedPredicate on the Gateway,
251- // which enqueues a full re-translation so the extension server can re-apply
252- // the correct online/offline routing config from its informer cache.
253- func (r * ConnectorReconciler ) touchDownstreamGatewayAnnotations (
254- ctx context.Context ,
255- upstreamClient client.Client ,
256- clusterName string ,
257- connector * networkingv1alpha1.Connector ,
258- ) error {
259- logger := log .FromContext (ctx )
260-
261- // List all HTTPProxies in the Connector's namespace; filter to those that
262- // reference this Connector. The HTTPProxy controller creates one Gateway
263- // per HTTPProxy with the same name and namespace, so Gateway affinity
264- // follows directly from the HTTPProxy→Connector reference.
265- var httpProxies networkingv1alpha.HTTPProxyList
266- if err := upstreamClient .List (ctx , & httpProxies , client .InNamespace (connector .Namespace )); err != nil {
267- return fmt .Errorf ("list HTTPProxies in %s: %w" , connector .Namespace , err )
268- }
269-
270- // Resolve the downstream namespace once for the whole batch.
271- strategy := downstreamclient .NewMappedNamespaceResourceStrategy (
272- clusterName ,
273- upstreamClient ,
274- r .DownstreamCluster .GetClient (),
275- )
276- downstreamNS , err := strategy .GetDownstreamNamespaceNameForUpstreamNamespace (ctx , connector .Namespace )
277- if err != nil {
278- return fmt .Errorf ("get downstream namespace for %s: %w" , connector .Namespace , err )
279- }
280-
281- readyCondition := apimeta .FindStatusCondition (connector .Status .Conditions , networkingv1alpha1 .ConnectorConditionReady )
282- annotationValue := connectorReadyAnnotationValue (readyCondition )
283- downstreamCl := r .DownstreamCluster .GetClient ()
284-
285- for i := range httpProxies .Items {
286- hp := & httpProxies .Items [i ]
287- if ! httpProxyReferencesConnector (hp , connector .Name ) {
288- continue
289- }
290-
291- // Gateway name == HTTPProxy name (see HTTPProxyReconciler.collectDesiredResources).
292- gwKey := client.ObjectKey {Namespace : downstreamNS , Name : hp .Name }
293- var gw gatewayv1.Gateway
294- if err := downstreamCl .Get (ctx , gwKey , & gw ); err != nil {
295- if apierrors .IsNotFound (err ) {
296- // Gateway not yet created or already deleted — skip.
297- logger .V (1 ).Info ("downstream gateway not found; skipping annotation touch" ,
298- "gateway" , gwKey )
299- continue
300- }
301- return fmt .Errorf ("get downstream gateway %s: %w" , gwKey , err )
302- }
303-
304- // Idempotent: skip Patch if the value hasn't changed.
305- if gw .Annotations [connectorReadyAnnotationKey ] == annotationValue {
306- continue
307- }
308-
309- patch := client .MergeFrom (gw .DeepCopy ())
310- if gw .Annotations == nil {
311- gw .Annotations = make (map [string ]string )
312- }
313- gw .Annotations [connectorReadyAnnotationKey ] = annotationValue
314- if err := downstreamCl .Patch (ctx , & gw , patch ); err != nil {
315- return fmt .Errorf ("patch downstream gateway %s annotation: %w" , gwKey , err )
316- }
317- logger .Info ("touched downstream gateway annotation for connector ready state change" ,
318- "gateway" , gwKey ,
319- "connector" , connector .Name ,
320- "annotationValue" , annotationValue )
321- }
322-
323- return nil
324- }
325-
326183func (r * ConnectorReconciler ) connectorLeaseDurationSeconds () int32 {
327184 if r .Config .Connector .LeaseDurationSeconds > 0 {
328185 return r .Config .Connector .LeaseDurationSeconds
0 commit comments