Skip to content

Commit c90c396

Browse files
authored
feat: TargetNamespaced support in graph indexer (#30)
* feat: support TargetNamespaced in graph indexer for cluster-scoped targets * test: add integration test for cluster-scoped target edges * refactor: remove unused deferredEdges from graph engine * chore: bump plugin-sdk to v0.4.0 for TargetNamespaced support
1 parent 2c7a95a commit c90c396

7 files changed

Lines changed: 143 additions & 15 deletions

File tree

backend/pkg/plugin/resource/graph/graph.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ type RelationshipGraph struct {
1515
declarations map[declMapKey][]resource.RelationshipDescriptor
1616
pluginDecls map[string][]declMapKey
1717
selectorCache map[string]map[string]string
18-
deferredEdges map[string][]deferredEdge
1918
strings *stringInterner
2019
}
2120

@@ -26,7 +25,6 @@ func NewRelationshipGraph() *RelationshipGraph {
2625
declarations: make(map[declMapKey][]resource.RelationshipDescriptor),
2726
pluginDecls: make(map[string][]declMapKey),
2827
selectorCache: make(map[string]map[string]string),
29-
deferredEdges: make(map[string][]deferredEdge),
3028
strings: newStringInterner(),
3129
}
3230
}

backend/pkg/plugin/resource/graph/graph_indexer.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,17 @@ func (gi *GraphIndexer) extractFieldPath(source GraphNode, entry registry.Resour
8787
}
8888

8989
for _, targetID := range targetIDs {
90+
targetNS := entry.Namespace
91+
if decl.TargetNamespaced != nil && !*decl.TargetNamespaced {
92+
targetNS = ""
93+
}
94+
9095
target := GraphNode{
9196
PluginID: entry.PluginID,
9297
ConnectionID: entry.ConnectionID,
9398
ResourceKey: decl.TargetResourceKey,
9499
ID: targetID,
95-
Namespace: entry.Namespace,
100+
Namespace: targetNS,
96101
}
97102

98103
// EdgeIncoming reverses edge direction (used for ownership)

backend/pkg/plugin/resource/graph/graph_indexer_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,3 +381,77 @@ func TestGraphIndexer_LabelSelector_LabelUpdateBreaksMatch(t *testing.T) {
381381
t.Fatalf("expected 0 edges after pod label change, got %d: %v", len(edges), edges)
382382
}
383383
}
384+
385+
// TestGraphIndexer_FieldPath_ClusterScopedTarget — TargetNamespaced=false gives empty namespace.
386+
func TestGraphIndexer_FieldPath_ClusterScopedTarget(t *testing.T) {
387+
g, _, idx := setupIndexer()
388+
389+
clusterScoped := false
390+
g.SetDeclarations("k8s", "core::v1::Pod", []resource.RelationshipDescriptor{
391+
{
392+
Type: resource.RelRunsOn,
393+
TargetResourceKey: "core::v1::Node",
394+
Label: "runs on",
395+
TargetNamespaced: &clusterScoped,
396+
Extractor: &resource.RelationshipExtractor{
397+
Method: "fieldPath",
398+
FieldPath: "spec.nodeName",
399+
},
400+
},
401+
})
402+
403+
pod := makeEntry("core::v1::Pod", "default", "nginx")
404+
raw := json.RawMessage(`{"spec":{"nodeName":"worker-1"}}`)
405+
406+
idx.OnAdd(pod, raw)
407+
408+
podNode := GraphNode{
409+
PluginID: "k8s", ConnectionID: "c1",
410+
ResourceKey: "core::v1::Pod", ID: "nginx", Namespace: "default",
411+
}
412+
edges := g.EdgesFrom(podNode.Key())
413+
if len(edges) != 1 {
414+
t.Fatalf("expected 1 edge, got %d", len(edges))
415+
}
416+
e := edges[0]
417+
if e.Target.Namespace != "" {
418+
t.Errorf("expected empty namespace for cluster-scoped target, got %q", e.Target.Namespace)
419+
}
420+
if e.Target.ID != "worker-1" {
421+
t.Errorf("expected target ID 'worker-1', got %q", e.Target.ID)
422+
}
423+
}
424+
425+
// TestGraphIndexer_FieldPath_NamespacedTargetDefault — nil TargetNamespaced inherits source namespace.
426+
func TestGraphIndexer_FieldPath_NamespacedTargetDefault(t *testing.T) {
427+
g, _, idx := setupIndexer()
428+
429+
g.SetDeclarations("k8s", "core::v1::Pod", []resource.RelationshipDescriptor{
430+
{
431+
Type: resource.RelUses,
432+
TargetResourceKey: "core::v1::ConfigMap",
433+
Label: "uses",
434+
Extractor: &resource.RelationshipExtractor{
435+
Method: "fieldPath",
436+
FieldPath: "spec.volumes.0.configMap.name",
437+
},
438+
},
439+
})
440+
441+
pod := makeEntry("core::v1::Pod", "production", "app-pod")
442+
raw := json.RawMessage(`{"spec":{"volumes":[{"configMap":{"name":"app-config"}}]}}`)
443+
444+
idx.OnAdd(pod, raw)
445+
446+
podNode := GraphNode{
447+
PluginID: "k8s", ConnectionID: "c1",
448+
ResourceKey: "core::v1::Pod", ID: "app-pod", Namespace: "production",
449+
}
450+
edges := g.EdgesFrom(podNode.Key())
451+
if len(edges) != 1 {
452+
t.Fatalf("expected 1 edge, got %d", len(edges))
453+
}
454+
if edges[0].Target.Namespace != "production" {
455+
t.Errorf("expected namespace 'production', got %q", edges[0].Target.Namespace)
456+
}
457+
}

backend/pkg/plugin/resource/graph/types.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,3 @@ type DependencyNode struct {
5656
Edge GraphEdge `json:"edge"`
5757
Children []DependencyNode `json:"children"`
5858
}
59-
60-
type deferredEdge struct {
61-
Source GraphNode
62-
Type RelationshipType
63-
Label string
64-
TargetKey string
65-
TargetID string
66-
TargetNS string
67-
}

backend/pkg/plugin/resource/graph_integration_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,66 @@ func TestIntegration_OwnershipChain(t *testing.T) {
168168
}
169169
}
170170

171+
func TestIntegration_ClusterScopedTarget(t *testing.T) {
172+
store := registry.NewMemoryStore()
173+
g := graph.NewRelationshipGraph()
174+
graphIdx := graph.NewGraphIndexer(g, store)
175+
disp := indexer.NewDispatcher([]indexer.ResourceIndexer{graphIdx})
176+
disp.Start()
177+
defer disp.Stop()
178+
179+
ctrl, _ := newTestControllerWithEmitter(t)
180+
ctrl.registryStore = store
181+
ctrl.dispatcher = disp
182+
ctrl.graph = g
183+
184+
sink := &engineWatchSink{
185+
pluginID: "k8s", ctrl: ctrl,
186+
store: store, dispatcher: disp,
187+
}
188+
189+
clusterScoped := false
190+
g.SetDeclarations("k8s", "core::v1::PersistentVolumeClaim", []sdkresource.RelationshipDescriptor{
191+
{
192+
Type: sdkresource.RelUses,
193+
TargetResourceKey: "core::v1::PersistentVolume",
194+
Label: "uses",
195+
TargetNamespaced: &clusterScoped,
196+
Extractor: &sdkresource.RelationshipExtractor{
197+
Method: "fieldPath",
198+
FieldPath: "spec.volumeName",
199+
},
200+
},
201+
})
202+
203+
sink.OnAdd(sdkresource.WatchAddPayload{
204+
Key: "core::v1::PersistentVolumeClaim", Connection: "c1",
205+
ID: "data-pvc", Namespace: "default",
206+
Metadata: sdkresource.ResourceMetadata{UID: "pvc-uid-1"},
207+
Data: json.RawMessage(`{"spec":{"volumeName":"pv-0001"}}`),
208+
})
209+
210+
disp.Flush()
211+
212+
pvcNode := graph.GraphNode{
213+
PluginID: "k8s", ConnectionID: "c1",
214+
ResourceKey: "core::v1::PersistentVolumeClaim", ID: "data-pvc", Namespace: "default",
215+
}
216+
edges, err := g.GetRelated(pvcNode, graph.Outgoing, nil)
217+
if err != nil {
218+
t.Fatal(err)
219+
}
220+
if len(edges) != 1 {
221+
t.Fatalf("expected 1 edge, got %d", len(edges))
222+
}
223+
if edges[0].Target.Namespace != "" {
224+
t.Errorf("expected empty namespace for PV target, got %q", edges[0].Target.Namespace)
225+
}
226+
if edges[0].Target.ID != "pv-0001" {
227+
t.Errorf("expected target pv-0001, got %s", edges[0].Target.ID)
228+
}
229+
}
230+
171231
func TestIntegration_ConnectionTeardown(t *testing.T) {
172232
store := registry.NewMemoryStore()
173233
g := graph.NewRelationshipGraph()

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ require (
1212
github.com/hashicorp/go-hclog v1.6.3
1313
github.com/hashicorp/go-plugin v1.7.0
1414
github.com/nxadm/tail v1.4.11
15-
github.com/omniviewdev/plugin-sdk v0.3.1
15+
github.com/omniviewdev/plugin-sdk v0.4.0
1616
github.com/omniviewdev/registry v0.2.1
1717
github.com/stretchr/testify v1.11.1
1818
github.com/tidwall/gjson v1.18.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
131131
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
132132
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
133133
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
134-
github.com/omniviewdev/plugin-sdk v0.3.1 h1:VccZy6l8P05qay5Gut3NxgWr3xuFJidIC+qH8BT9bx0=
135-
github.com/omniviewdev/plugin-sdk v0.3.1/go.mod h1:7EL5BfctDEQdflClkZMhgvSAGkDff79UteN7rcuBUho=
134+
github.com/omniviewdev/plugin-sdk v0.4.0 h1:0W9HTWTUYHv5s0yhU6KpxcPdEqLG7JfpkNjcGDllXmA=
135+
github.com/omniviewdev/plugin-sdk v0.4.0/go.mod h1:7EL5BfctDEQdflClkZMhgvSAGkDff79UteN7rcuBUho=
136136
github.com/omniviewdev/registry v0.2.1 h1:4CsiBZmlftBZV/3LyQNiI2plRkMAqgFD8Q4zffFCVYk=
137137
github.com/omniviewdev/registry v0.2.1/go.mod h1:/IZABypY6iIaHo2Gw5g5Ll4SIbUhEm5/02spR/svl3Q=
138138
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=

0 commit comments

Comments
 (0)