Skip to content

Commit 1d645ee

Browse files
scotwellsclaude
andcommitted
fix(connector): propagate liveness Project→member via annotation
Connector Ready + connectionDetails are computed authoritatively in the Project control plane and stored in the Connector status subresource. The edge extension server runs on a member cluster and reads connectors from the local cache to classify a tunnel online/offline. The replicator's downstream write lands on the Karmada hub resourcetemplate, and Karmada propagates only spec + metadata (labels/annotations) to members — NOT the status subresource. So the member-cluster Connector carries spec only, every connector reads as offline on the data plane, and online promotion can never fire. Karmada has no native push-status-down mechanism, but it does propagate template annotations to members. Carry liveness down through an annotation instead of status: - Replicator stamps networking.datumapis.com/connector-liveness onto the downstream Connector metadata (a compact JSON snapshot: ready + nodeID), written via the normal CreateOrUpdate Update (not the status subresource). - Extension server reads the annotation first and falls back to status when it is absent/unparseable (single-cluster and pre-rollout objects). The annotation carries only status-derived fields the ext-server consumes: ready (from the Ready condition) and nodeID (from status.connectionDetails.publicKey.id). TargetHost/TargetPort are NOT carried because they derive from the HTTPProxy backend endpoint, not connector status. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8b8cda1 commit 1d645ee

6 files changed

Lines changed: 410 additions & 57 deletions

File tree

api/v1alpha1/connector_types.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,41 @@ const (
181181

182182
const ConnectorNameAnnotation = "networking.datum.org/connector-name"
183183

184+
// ConnectorLivenessAnnotation carries a compact snapshot of a Connector's
185+
// authoritative upstream liveness down to edge member clusters.
186+
//
187+
// A Connector's Ready condition and ConnectionDetails are computed in the
188+
// Project control plane and stored in the Connector's status subresource. The
189+
// edge extension server reads connectors from the local member-cluster cache to
190+
// decide whether a tunnel is online. Karmada propagates a resource template's
191+
// spec and metadata (labels/annotations) to member clusters but NOT the status
192+
// subresource, so the member-cluster Connector never carries Ready or
193+
// ConnectionDetails. To bridge that gap the replicator stamps the liveness onto
194+
// this annotation — which Karmada DOES propagate — and the extension server
195+
// reads it from there, falling back to status when the annotation is absent.
196+
//
197+
// The value is a JSON-marshalled ConnectorLiveness. Keep the JSON schema in
198+
// sync with that type.
199+
const ConnectorLivenessAnnotation = "networking.datumapis.com/connector-liveness"
200+
201+
// ConnectorLiveness is the JSON payload stored in ConnectorLivenessAnnotation.
202+
//
203+
// It carries only the status-derived fields the extension server needs to
204+
// classify a connector online/offline and to build the data-plane tunnel
205+
// cluster. It deliberately does NOT include the tunnel TargetHost/TargetPort:
206+
// those are derived from the referencing HTTPProxy backend endpoint URL, not
207+
// from Connector status.
208+
type ConnectorLiveness struct {
209+
// Ready mirrors the upstream Connector's Ready condition being True.
210+
Ready bool `json:"ready"`
211+
212+
// NodeID is the connector's public-key id, taken from
213+
// Status.ConnectionDetails.PublicKey.Id when the connector is ready and
214+
// advertises PublicKey connection details. Empty otherwise. Used as the
215+
// tunnel endpoint_id in the data-plane connector cluster.
216+
NodeID string `json:"nodeID,omitempty"`
217+
}
218+
184219
// +kubebuilder:object:root=true
185220
// +kubebuilder:subresource:status
186221
// +kubebuilder:selectablefield:JSONPath=".status.connectionDetails.publicKey.id"

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/controller/gateway_resource_replicator_controller.go

Lines changed: 98 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package controller
44

55
import (
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+
430518
func (r *GatewayResourceReplicatorReconciler) syncUpstreamStatus(
431519
ctx context.Context,
432520
resource replicationResource,

0 commit comments

Comments
 (0)