Skip to content

Commit eaa98e2

Browse files
committed
feat(epp): extend topology-extractor with zone and region
Add zone and region fields to the Topology attribute and the params struct. Each param names the pod label to read; defaults are the standard Kubernetes topology labels (corev1.LabelHostname, LabelTopologyZone, LabelTopologyRegion). The hostname label falling back to spec.hostname is preserved: when the hostname label is absent the endpoint is tracked and stamped once the Pod notification fires. Zone and region have no fallback. Zone and region values are not read from Node objects -- that would require RBAC for Node GET/LIST. They are populated when the pod itself carries the corresponding labels. Signed-off-by: Etai Lev Ran <elevran@gmail.com>
1 parent a79f721 commit eaa98e2

3 files changed

Lines changed: 199 additions & 47 deletions

File tree

pkg/epp/framework/plugins/datalayer/attribute/topology/data_types.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,15 @@ const (
3232
var TopologyAttributeKey = plugin.NewDataKey("Topology", TopologyExtractorType)
3333

3434
// Topology carries the locality information for an endpoint.
35-
// The Hostname field is populated once when the endpoint is created and
36-
// does not change unless the endpoint is re-created.
35+
// Fields are populated from pod labels at endpoint creation time.
36+
// Hostname falls back to spec.hostname when the configured label is absent.
3737
type Topology struct {
38-
// Hostname identifies the node or host on which the endpoint runs.
39-
// Derived from the Pod's hostname field or from a user-configured label.
38+
// Hostname identifies the node on which the endpoint runs.
4039
Hostname string
40+
// Zone identifies the failure domain zone of the endpoint.
41+
Zone string
42+
// Region identifies the geographic region of the endpoint.
43+
Region string
4144
}
4245

4346
// Clone returns an independent copy of the Topology.

pkg/epp/framework/plugins/datalayer/extractor/topology/extractor.go

Lines changed: 69 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,13 @@ limitations under the License.
1515
*/
1616

1717
// Package topology provides a datalayer plugin that stamps each endpoint with
18-
// a Topology attribute containing the endpoint's hostname.
18+
// a Topology attribute containing locality fields (hostname, zone, region).
1919
//
20-
// The plugin registers for both endpoint lifecycle events and Pod k8s notification
21-
// events. When hostnameLabel is configured, the label value is read from the
22-
// endpoint event's pod metadata and written as the Topology attribute; Pod
23-
// notifications are a no-op. When hostnameLabel is empty, the endpoint event
24-
// tracks live endpoints keyed by pod identity; the Pod notification (ready pods
25-
// only) reads spec.hostname, caches it, and stamps all current endpoints for
26-
// that pod. Whichever event fires first, the attribute is set once both have
27-
// been processed.
20+
// Label names for each field are resolved from plugin parameters, defaulting to
21+
// the standard Kubernetes topology labels. Labels are read from the endpoint's
22+
// pod metadata at endpoint event time. When the hostname label is absent from
23+
// the pod, the plugin falls back to spec.hostname from Pod notification events.
24+
// Zone and region have no fallback.
2825
package topology
2926

3027
import (
@@ -33,6 +30,7 @@ import (
3330
"slices"
3431
"sync"
3532

33+
corev1 "k8s.io/api/core/v1"
3634
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3735
"k8s.io/apimachinery/pkg/runtime/schema"
3836
"k8s.io/apimachinery/pkg/types"
@@ -53,25 +51,43 @@ var (
5351
// podGVK is the core/v1 Pod GVK watched by the pod notification handler.
5452
var podGVK = schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}
5553

54+
const (
55+
defaultHostnameLabel = corev1.LabelHostname
56+
defaultZoneLabel = corev1.LabelTopologyZone
57+
defaultRegionLabel = corev1.LabelTopologyRegion
58+
)
59+
5660
// params holds the user-facing configuration for the topology extractor.
61+
// Each field names the pod label used to read the corresponding topology value.
62+
// When a field is empty, the corresponding default Kubernetes topology label is used.
63+
// When the hostname label is absent on a pod, spec.hostname is used as a fallback.
64+
// Zone and region have no fallback.
5765
type params struct {
58-
// HostnameLabel is the pod label whose value is used as the topology hostname.
59-
// When empty (default), spec.hostname from the Pod object is used instead.
60-
// When set but absent on the pod, the attribute is not added.
61-
HostnameLabel string `json:"hostnameLabel,omitempty"`
66+
// Hostname is the pod label name whose value is used as the topology hostname.
67+
// Defaults to kubernetes.io/hostname.
68+
Hostname string `json:"hostname,omitempty"`
69+
// Zone is the pod label name whose value is used as the topology zone.
70+
// Defaults to topology.kubernetes.io/zone.
71+
Zone string `json:"zone,omitempty"`
72+
// Region is the pod label name whose value is used as the topology region.
73+
// Defaults to topology.kubernetes.io/region.
74+
Region string `json:"region,omitempty"`
6275
}
6376

6477
// TopologyExtractor stamps each endpoint with a Topology attribute.
6578
// It registers for both endpoint lifecycle events and Pod k8s notifications.
6679
type TopologyExtractor struct {
6780
typedName fwkplugin.TypedName
6881
hostnameLabel string
82+
zoneLabel string
83+
regionLabel string
6984
dk fwkplugin.DataKey
7085

7186
// mu guards endpoints and hostnames.
7287
mu sync.Mutex
7388
// endpoints maps pod identity to all live Endpoints for that pod.
7489
// Keyed by {PodName, Namespace} — one pod may have N rank endpoints.
90+
// Only endpoints whose hostname label was absent are tracked here.
7591
endpoints map[types.NamespacedName][]fwkdl.Endpoint
7692
// hostnames caches spec.hostname per pod (ready pods only).
7793
// Populated by Pod notifications; cleared on pod delete.
@@ -86,12 +102,23 @@ func Factory(name string, parameters *json.Decoder, _ fwkplugin.Handle) (fwkplug
86102
return nil, err
87103
}
88104
}
105+
if p.Hostname == "" {
106+
p.Hostname = defaultHostnameLabel
107+
}
108+
if p.Zone == "" {
109+
p.Zone = defaultZoneLabel
110+
}
111+
if p.Region == "" {
112+
p.Region = defaultRegionLabel
113+
}
89114
if name == "" {
90115
name = attrtopology.TopologyExtractorType
91116
}
92117
return &TopologyExtractor{
93118
typedName: fwkplugin.TypedName{Type: attrtopology.TopologyExtractorType, Name: name},
94-
hostnameLabel: p.HostnameLabel,
119+
hostnameLabel: p.Hostname,
120+
zoneLabel: p.Zone,
121+
regionLabel: p.Region,
95122
dk: attrtopology.TopologyAttributeKey.WithNonEmptyProducerName(name),
96123
endpoints: make(map[types.NamespacedName][]fwkdl.Endpoint),
97124
hostnames: make(map[types.NamespacedName]string),
@@ -157,31 +184,31 @@ func (h *endpointHandler) Extract(_ context.Context, event fwkdl.EndpointEvent)
157184
}
158185

159186
if event.Type == fwkdl.EventDelete {
160-
if h.ext.hostnameLabel == "" {
161-
h.removeEndpoint(meta, event.Endpoint)
162-
}
187+
h.removeEndpoint(meta, event.Endpoint)
163188
return nil
164189
}
165190

166-
if h.ext.hostnameLabel != "" {
167-
val, ok := meta.Labels[h.ext.hostnameLabel]
168-
if !ok || val == "" {
169-
return nil
170-
}
171-
event.Endpoint.GetAttributes().Put(h.ext.dk.String(), &attrtopology.Topology{Hostname: val})
172-
return nil
173-
}
191+
hn := meta.Labels[h.ext.hostnameLabel]
192+
zone := meta.Labels[h.ext.zoneLabel]
193+
region := meta.Labels[h.ext.regionLabel]
174194

175-
// No-label path: track endpoint; stamp immediately if hostname already cached.
176-
key := podKey(meta)
177-
h.ext.mu.Lock()
178-
h.ext.endpoints[key] = append(h.ext.endpoints[key], event.Endpoint)
179-
hn := h.ext.hostnames[key]
180-
h.ext.mu.Unlock()
195+
if hn == "" {
196+
// Hostname label absent: track endpoint for spec.hostname fallback via pod notification.
197+
key := podKey(meta)
198+
h.ext.mu.Lock()
199+
h.ext.endpoints[key] = append(h.ext.endpoints[key], event.Endpoint)
200+
hn = h.ext.hostnames[key]
201+
h.ext.mu.Unlock()
202+
}
181203

182-
if hn != "" {
183-
event.Endpoint.GetAttributes().Put(h.ext.dk.String(), &attrtopology.Topology{Hostname: hn})
204+
if hn == "" && zone == "" && region == "" {
205+
return nil
184206
}
207+
event.Endpoint.GetAttributes().Put(h.ext.dk.String(), &attrtopology.Topology{
208+
Hostname: hn,
209+
Zone: zone,
210+
Region: region,
211+
})
185212
return nil
186213
}
187214

@@ -224,10 +251,6 @@ func (h *podNotificationHandler) GVK() schema.GroupVersionKind {
224251
}
225252

226253
func (h *podNotificationHandler) Extract(_ context.Context, event fwkdl.NotificationEvent) error {
227-
if h.ext.hostnameLabel != "" {
228-
return nil
229-
}
230-
231254
obj := event.Object
232255
key := types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}
233256

@@ -249,7 +272,15 @@ func (h *podNotificationHandler) Extract(_ context.Context, event fwkdl.Notifica
249272
h.ext.mu.Unlock()
250273

251274
for _, ep := range eps {
252-
ep.GetAttributes().Put(h.ext.dk.String(), &attrtopology.Topology{Hostname: hostname})
275+
// Preserve zone/region already set from the endpoint event.
276+
topo := &attrtopology.Topology{Hostname: hostname}
277+
if raw, ok := ep.GetAttributes().Get(h.ext.dk.String()); ok {
278+
if existing, ok := raw.(*attrtopology.Topology); ok {
279+
topo.Zone = existing.Zone
280+
topo.Region = existing.Region
281+
}
282+
}
283+
ep.GetAttributes().Put(h.ext.dk.String(), topo)
253284
}
254285
return nil
255286
}

pkg/epp/framework/plugins/datalayer/extractor/topology/extractor_test.go

Lines changed: 123 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ func podEvent(t fwkdl.EventType, pod *unstructured.Unstructured) fwkdl.Notificat
148148
// --- label-configured path ---
149149

150150
func TestLabel_EndpointHandler_LabelPresent(t *testing.T) {
151-
_, epH, _ := getHandlers(t, &params{HostnameLabel: "topology.hostname"})
151+
_, epH, _ := getHandlers(t, &params{Hostname: "topology.hostname"})
152152

153153
ep := newRankEndpoint("worker-1", 0, map[string]string{"topology.hostname": "rack-42"})
154154
if err := epH.Extract(context.Background(), epEvent(fwkdl.EventAddOrUpdate, ep)); err != nil {
@@ -165,7 +165,7 @@ func TestLabel_EndpointHandler_LabelPresent(t *testing.T) {
165165
}
166166

167167
func TestLabel_EndpointHandler_LabelMissing(t *testing.T) {
168-
_, epH, _ := getHandlers(t, &params{HostnameLabel: "topology.hostname"})
168+
_, epH, _ := getHandlers(t, &params{Hostname: "topology.hostname"})
169169

170170
ep := newRankEndpoint("worker-2", 0, map[string]string{"other": "value"})
171171
if err := epH.Extract(context.Background(), epEvent(fwkdl.EventAddOrUpdate, ep)); err != nil {
@@ -177,7 +177,7 @@ func TestLabel_EndpointHandler_LabelMissing(t *testing.T) {
177177
}
178178

179179
func TestLabel_EndpointHandler_NilLabels(t *testing.T) {
180-
_, epH, _ := getHandlers(t, &params{HostnameLabel: "topology.hostname"})
180+
_, epH, _ := getHandlers(t, &params{Hostname: "topology.hostname"})
181181

182182
ep := newRankEndpoint("worker-3", 0, nil)
183183
if err := epH.Extract(context.Background(), epEvent(fwkdl.EventAddOrUpdate, ep)); err != nil {
@@ -189,7 +189,7 @@ func TestLabel_EndpointHandler_NilLabels(t *testing.T) {
189189
}
190190

191191
func TestLabel_EndpointHandler_DeleteIsNoop(t *testing.T) {
192-
_, epH, _ := getHandlers(t, &params{HostnameLabel: "topology.hostname"})
192+
_, epH, _ := getHandlers(t, &params{Hostname: "topology.hostname"})
193193

194194
ep := newRankEndpoint("worker-4", 0, map[string]string{"topology.hostname": "rack-1"})
195195
if err := epH.Extract(context.Background(), epEvent(fwkdl.EventDelete, ep)); err != nil {
@@ -201,7 +201,7 @@ func TestLabel_EndpointHandler_DeleteIsNoop(t *testing.T) {
201201
}
202202

203203
func TestLabel_PodHandler_IsNoop(t *testing.T) {
204-
_, _, podH := getHandlers(t, &params{HostnameLabel: "topology.hostname"})
204+
_, _, podH := getHandlers(t, &params{Hostname: "topology.hostname"})
205205

206206
pod := makePod("worker-1", "actual-hostname", true)
207207
if err := podH.Extract(context.Background(), podEvent(fwkdl.EventAddOrUpdate, pod)); err != nil {
@@ -455,6 +455,124 @@ func TestNoLabel_EndpointDelete_RemovesFromMap(t *testing.T) {
455455
}
456456
}
457457

458+
// --- zone and region label extraction ---
459+
460+
func TestLabel_ZoneAndRegion(t *testing.T) {
461+
_, epH, _ := getHandlers(t, &params{
462+
Hostname: "my/hostname",
463+
Zone: "my/zone",
464+
Region: "my/region",
465+
})
466+
467+
ep := newRankEndpoint("worker-z1", 0, map[string]string{
468+
"my/hostname": "host-1",
469+
"my/zone": "us-east-1a",
470+
"my/region": "us-east-1",
471+
})
472+
if err := epH.Extract(context.Background(), epEvent(fwkdl.EventAddOrUpdate, ep)); err != nil {
473+
t.Fatalf("Extract: %v", err)
474+
}
475+
476+
got, ok := readTopology(ep)
477+
if !ok {
478+
t.Fatal("expected Topology attribute")
479+
}
480+
if got.Hostname != "host-1" {
481+
t.Errorf("hostname = %q, want %q", got.Hostname, "host-1")
482+
}
483+
if got.Zone != "us-east-1a" {
484+
t.Errorf("zone = %q, want %q", got.Zone, "us-east-1a")
485+
}
486+
if got.Region != "us-east-1" {
487+
t.Errorf("region = %q, want %q", got.Region, "us-east-1")
488+
}
489+
}
490+
491+
func TestLabel_ZoneAndRegionDefault(t *testing.T) {
492+
_, epH, _ := getHandlers(t, nil)
493+
494+
ep := newRankEndpoint("worker-z2", 0, map[string]string{
495+
defaultHostnameLabel: "host-2",
496+
defaultZoneLabel: "eu-west-1b",
497+
defaultRegionLabel: "eu-west-1",
498+
})
499+
if err := epH.Extract(context.Background(), epEvent(fwkdl.EventAddOrUpdate, ep)); err != nil {
500+
t.Fatalf("Extract: %v", err)
501+
}
502+
503+
got, ok := readTopology(ep)
504+
if !ok {
505+
t.Fatal("expected Topology attribute")
506+
}
507+
if got.Hostname != "host-2" {
508+
t.Errorf("hostname = %q, want %q", got.Hostname, "host-2")
509+
}
510+
if got.Zone != "eu-west-1b" {
511+
t.Errorf("zone = %q, want %q", got.Zone, "eu-west-1b")
512+
}
513+
if got.Region != "eu-west-1" {
514+
t.Errorf("region = %q, want %q", got.Region, "eu-west-1")
515+
}
516+
}
517+
518+
func TestLabel_ZoneOnlyNoHostname_FallsBackToSpecHostname(t *testing.T) {
519+
_, epH, podH := getHandlers(t, &params{Zone: "my/zone"})
520+
521+
// Endpoint has zone label but not the hostname label (defaults to kubernetes.io/hostname).
522+
ep := newRankEndpoint("worker-z3", 0, map[string]string{
523+
"my/zone": "ap-south-1a",
524+
})
525+
if err := epH.Extract(context.Background(), epEvent(fwkdl.EventAddOrUpdate, ep)); err != nil {
526+
t.Fatalf("epH.Extract: %v", err)
527+
}
528+
529+
// Zone set but hostname still empty — pod notification provides the fallback.
530+
pod := makePod("worker-z3", testNodeHostname, true)
531+
if err := podH.Extract(context.Background(), podEvent(fwkdl.EventAddOrUpdate, pod)); err != nil {
532+
t.Fatalf("podH.Extract: %v", err)
533+
}
534+
535+
got, ok := readTopology(ep)
536+
if !ok {
537+
t.Fatal("expected Topology attribute")
538+
}
539+
if got.Hostname != testNodeHostname {
540+
t.Errorf("hostname = %q, want %q", got.Hostname, testNodeHostname)
541+
}
542+
if got.Zone != "ap-south-1a" {
543+
t.Errorf("zone = %q, want %q", got.Zone, "ap-south-1a")
544+
}
545+
}
546+
547+
func TestLabel_HostnameLabelAbsent_FallsBackToSpecHostname(t *testing.T) {
548+
_, epH, podH := getHandlers(t, &params{Hostname: "custom/hostname"})
549+
550+
// Endpoint does not have the configured hostname label.
551+
ep := newRankEndpoint("worker-z4", 0, map[string]string{"other": "val"})
552+
if err := epH.Extract(context.Background(), epEvent(fwkdl.EventAddOrUpdate, ep)); err != nil {
553+
t.Fatalf("epH.Extract: %v", err)
554+
}
555+
556+
// No attribute yet.
557+
if _, ok := readTopology(ep); ok {
558+
t.Error("expected no Topology attribute before pod notification")
559+
}
560+
561+
// Pod notification provides spec.hostname fallback.
562+
pod := makePod("worker-z4", testNodeHostname, true)
563+
if err := podH.Extract(context.Background(), podEvent(fwkdl.EventAddOrUpdate, pod)); err != nil {
564+
t.Fatalf("podH.Extract: %v", err)
565+
}
566+
567+
got, ok := readTopology(ep)
568+
if !ok {
569+
t.Fatal("expected Topology attribute after pod notification")
570+
}
571+
if got.Hostname != testNodeHostname {
572+
t.Errorf("hostname = %q, want %q", got.Hostname, testNodeHostname)
573+
}
574+
}
575+
458576
// --- registration ---
459577

460578
func TestRegisterDependencies_RegistersBothHandlers(t *testing.T) {

0 commit comments

Comments
 (0)