@@ -4,6 +4,7 @@ package controller
44
55import (
66 "context"
7+ "encoding/json"
78 "fmt"
89 "strings"
910 "time"
@@ -35,6 +36,7 @@ import (
3536 mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile"
3637 mcsource "sigs.k8s.io/multicluster-runtime/pkg/source"
3738
39+ networkingv1alpha1 "go.datum.net/network-services-operator/api/v1alpha1"
3840 "go.datum.net/network-services-operator/internal/config"
3941 downstreamclient "go.datum.net/network-services-operator/internal/downstreamclient"
4042)
@@ -72,12 +74,27 @@ type replicationResourceConfig struct {
7274 conditionHandlers conditionReasonHandlers
7375
7476 // mirrorStatusDownstream copies upstream status → downstream status after
75- // each spec sync. Used for resource types (e.g. Connector) whose status is
76- // authoritative in the upstream cluster and must be readable by consumers
77- // in the downstream cluster (e.g. the extension server). When true,
78- // skipUpstreamStatusSync is implicitly honoured as well.
77+ // each spec sync. Used for resource types whose status is authoritative in
78+ // the upstream cluster and must be readable by consumers in the downstream
79+ // cluster. When true, skipUpstreamStatusSync is implicitly honoured as well.
80+ //
81+ // NOTE: this does NOT work across a Karmada hub→member boundary — Karmada
82+ // propagates a resource template's spec + metadata to members but NOT the
83+ // status subresource. For the Connector type (whose downstream consumer, the
84+ // edge extension server, lives on a member cluster) use
85+ // writeLivenessAnnotation instead, which rides an annotation Karmada does
86+ // propagate.
7987 mirrorStatusDownstream bool
8088
89+ // writeLivenessAnnotation, when true, derives a compact liveness snapshot
90+ // from the upstream Connector's status and stores it as the
91+ // ConnectorLivenessAnnotation on the downstream object's metadata. Karmada
92+ // propagates resource-template annotations (but NOT the status subresource)
93+ // to member clusters, so this is how connector liveness reaches the edge
94+ // extension server. Used instead of mirrorStatusDownstream for the Connector
95+ // type. Implies skipUpstreamStatusSync should also be set.
96+ writeLivenessAnnotation bool
97+
8198 // skipUpstreamStatusSync suppresses the normal downstream→upstream status
8299 // propagation. Set this for resource types where the upstream status is
83100 // managed by NSO's own controllers (not by a downstream controller), so
@@ -146,14 +163,17 @@ func initReplicationResourceConfigs() map[string]replicationResourceConfig {
146163 }
147164 }
148165
149- // Connector status (conditions + connectionDetails) is authoritative
150- // upstream and must be readable by the extension server downstream so it
151- // can determine whether a tunnel is online before injecting connector
152- // cluster patches. Mirror status downstream; do not propagate back.
166+ // Connector liveness (Ready condition + connectionDetails) is authoritative
167+ // upstream and must be readable by the edge extension server so it can
168+ // determine whether a tunnel is online before injecting connector cluster
169+ // patches. The extension server runs on a member cluster, and Karmada does
170+ // not propagate the status subresource hub→member — only spec + metadata.
171+ // Carry liveness down through an annotation (which Karmada DOES propagate)
172+ // instead of mirroring status. Do not propagate status back upstream.
153173 connectorGVK := schema.GroupVersionKind {Group : groupNetworkingDatumAPIs , Version : versionV1Alpha1 , Kind : KindConnector }
154174 configs [gvkKey (connectorGVK )] = replicationResourceConfig {
155- mirrorStatusDownstream : true ,
156- skipUpstreamStatusSync : true ,
175+ writeLivenessAnnotation : true ,
176+ skipUpstreamStatusSync : true ,
157177 }
158178
159179 return configs
@@ -314,6 +334,16 @@ func (r *GatewayResourceReplicatorReconciler) ensureDownstreamResource(
314334 return fmt .Errorf ("failed to set downstream controller reference: %w" , err )
315335 }
316336
337+ // Stamp the connector liveness annotation onto the downstream object's
338+ // metadata. This is part of the same CreateOrUpdate Update, so it is
339+ // persisted as ordinary metadata (which Karmada propagates to members) —
340+ // no status subresource write involved.
341+ if resource .replicationResourceConfig .writeLivenessAnnotation {
342+ if err := setConnectorLivenessAnnotation (downstreamObj , upstreamObj ); err != nil {
343+ return err
344+ }
345+ }
346+
317347 return nil
318348 })
319349 if err != nil {
@@ -427,6 +457,64 @@ func (r *GatewayResourceReplicatorReconciler) mirrorUpstreamStatusToDownstream(
427457 return nil
428458}
429459
460+ // setConnectorLivenessAnnotation derives a compact liveness snapshot from the
461+ // upstream Connector's status and stores it as ConnectorLivenessAnnotation on
462+ // the downstream object's metadata. It is the Karmada-friendly replacement for
463+ // mirroring the status subresource downstream: Karmada propagates a resource
464+ // template's metadata (annotations) to member clusters but not its status, so
465+ // the edge extension server reads liveness from this annotation instead.
466+ func setConnectorLivenessAnnotation (downstreamObj , upstreamObj * unstructured.Unstructured ) error {
467+ liveness , err := connectorLivenessFromUpstream (upstreamObj )
468+ if err != nil {
469+ return err
470+ }
471+
472+ raw , err := json .Marshal (liveness )
473+ if err != nil {
474+ return fmt .Errorf ("failed to marshal connector liveness: %w" , err )
475+ }
476+
477+ annotations := downstreamObj .GetAnnotations ()
478+ if annotations == nil {
479+ annotations = make (map [string ]string , 1 )
480+ }
481+ annotations [networkingv1alpha1 .ConnectorLivenessAnnotation ] = string (raw )
482+ downstreamObj .SetAnnotations (annotations )
483+ return nil
484+ }
485+
486+ // connectorLivenessFromUpstream extracts the Ready condition and PublicKey node
487+ // ID from an upstream Connector's (unstructured) status. A connector with no
488+ // status, a non-True Ready condition, or no PublicKey connection details yields
489+ // a not-ready liveness with an empty NodeID.
490+ func connectorLivenessFromUpstream (upstreamObj * unstructured.Unstructured ) (networkingv1alpha1.ConnectorLiveness , error ) {
491+ var liveness networkingv1alpha1.ConnectorLiveness
492+
493+ statusRaw , ok := upstreamObj .Object [jsonKeyStatus ]
494+ if ! ok {
495+ return liveness , nil
496+ }
497+ statusMap , ok := statusRaw .(map [string ]any )
498+ if ! ok {
499+ return liveness , nil
500+ }
501+
502+ var status networkingv1alpha1.ConnectorStatus
503+ if err := runtime .DefaultUnstructuredConverter .FromUnstructured (statusMap , & status ); err != nil {
504+ return liveness , fmt .Errorf ("failed to decode connector status: %w" , err )
505+ }
506+
507+ liveness .Ready = apimeta .IsStatusConditionTrue (status .Conditions , networkingv1alpha1 .ConnectorConditionReady )
508+ if liveness .Ready {
509+ if details := status .ConnectionDetails ; details != nil &&
510+ details .Type == networkingv1alpha1 .PublicKeyConnectorConnectionType &&
511+ details .PublicKey != nil {
512+ liveness .NodeID = details .PublicKey .Id
513+ }
514+ }
515+ return liveness , nil
516+ }
517+
430518func (r * GatewayResourceReplicatorReconciler ) syncUpstreamStatus (
431519 ctx context.Context ,
432520 resource replicationResource ,
0 commit comments