From cde6e45f58b41003edc28847866513f879399225 Mon Sep 17 00:00:00 2001 From: Ryszard Szwajlik Date: Mon, 23 Mar 2026 12:40:25 +0100 Subject: [PATCH 1/5] fix: add pod relationships to ConfigMaps and Secrets --- pkg/graph/core.go | 125 +++++++++++++++++++++++++++++++++++++++++++++ pkg/graph/graph.go | 15 ++++++ 2 files changed, 140 insertions(+) diff --git a/pkg/graph/core.go b/pkg/graph/core.go index 11ce2af..e280803 100644 --- a/pkg/graph/core.go +++ b/pkg/graph/core.go @@ -16,6 +16,7 @@ package graph import ( "context" + "fmt" "strings" v1 "k8s.io/api/core/v1" @@ -133,6 +134,86 @@ func (g *CoreV1Graph) Pod(pod *v1.Pod) (*Node, error) { g.graph.Relationship(n, "Container", c) } + for _, volume := range pod.Spec.Volumes { + if volume.ConfigMap != nil { + cm, err := g.ConfigMap(pod.GetNamespace(), volume.ConfigMap.Name) + if err != nil { + return nil, err + } + g.graph.Relationship(n, "ConfigMap", cm) + } + + if volume.Secret != nil { + secret, err := g.Secret(pod.GetNamespace(), volume.Secret.SecretName) + if err != nil { + return nil, err + } + g.graph.Relationship(n, "Secret", secret) + } + + if volume.Projected != nil { + for _, source := range volume.Projected.Sources { + if source.ConfigMap != nil { + cm, err := g.ConfigMap(pod.GetNamespace(), source.ConfigMap.Name) + if err != nil { + return nil, err + } + g.graph.Relationship(n, "ConfigMap", cm) + } + + if source.Secret != nil { + secret, err := g.Secret(pod.GetNamespace(), source.Secret.Name) + if err != nil { + return nil, err + } + g.graph.Relationship(n, "Secret", secret) + } + } + } + } + + for _, container := range append(pod.Spec.InitContainers, pod.Spec.Containers...) { + for _, envFrom := range container.EnvFrom { + if envFrom.ConfigMapRef != nil { + cm, err := g.ConfigMap(pod.GetNamespace(), envFrom.ConfigMapRef.Name) + if err != nil { + return nil, err + } + g.graph.Relationship(n, "ConfigMap", cm) + } + + if envFrom.SecretRef != nil { + secret, err := g.Secret(pod.GetNamespace(), envFrom.SecretRef.Name) + if err != nil { + return nil, err + } + g.graph.Relationship(n, "Secret", secret) + } + } + + for _, env := range container.Env { + if env.ValueFrom == nil { + continue + } + + if env.ValueFrom.ConfigMapKeyRef != nil { + cm, err := g.ConfigMap(pod.GetNamespace(), env.ValueFrom.ConfigMapKeyRef.Name) + if err != nil { + return nil, err + } + g.graph.Relationship(n, "ConfigMap", cm) + } + + if env.ValueFrom.SecretKeyRef != nil { + secret, err := g.Secret(pod.GetNamespace(), env.ValueFrom.SecretKeyRef.Name) + if err != nil { + return nil, err + } + g.graph.Relationship(n, "Secret", secret) + } + } + } + return n, nil } @@ -260,6 +341,50 @@ func (g *CoreV1Graph) Service(obj *v1.Service) (*Node, error) { return nil, nil } +// ConfigMap adds a v1.ConfigMap resource to the Graph or resolves an existing node for it. +func (g *CoreV1Graph) ConfigMap(namespace string, name string) (*Node, error) { + if name == "" { + return nil, fmt.Errorf("configmap reference is missing a name") + } + + if n := g.graph.FindNode(v1.SchemeGroupVersion.String(), "ConfigMap", namespace, name); n != nil { + return n, nil + } + + n := g.graph.Node( + schema.FromAPIVersionAndKind(v1.GroupName, "ConfigMap"), + &metav1.ObjectMeta{ + UID: ToUID("ConfigMap", namespace, name), + Namespace: namespace, + Name: name, + }, + ) + + return n, nil +} + +// Secret adds a v1.Secret resource to the Graph or resolves an existing node for it. +func (g *CoreV1Graph) Secret(namespace string, name string) (*Node, error) { + if name == "" { + return nil, fmt.Errorf("secret reference is missing a name") + } + + if n := g.graph.FindNode(v1.SchemeGroupVersion.String(), "Secret", namespace, name); n != nil { + return n, nil + } + + n := g.graph.Node( + schema.FromAPIVersionAndKind(v1.GroupName, "Secret"), + &metav1.ObjectMeta{ + UID: ToUID("Secret", namespace, name), + Namespace: namespace, + Name: name, + }, + ) + + return n, nil +} + // ServiceTypeClusterIP adds a v1.Service of type ClusterIP to the Graph. func (g *CoreV1Graph) ServiceTypeClusterIP(obj *v1.Service) (*Node, error) { n := g.graph.Node(schema.FromAPIVersionAndKind(v1.GroupName, "Service"), obj) diff --git a/pkg/graph/graph.go b/pkg/graph/graph.go index e21a62f..2cd8da2 100644 --- a/pkg/graph/graph.go +++ b/pkg/graph/graph.go @@ -177,6 +177,10 @@ func NewGraph(clientset *kubernetes.Clientset, objs []*unstructured.Unstructured errs := []error{} + for _, obj := range objs { + g.Node(obj.GroupVersionKind(), obj) + } + for _, obj := range objs { _, err := g.Unstructured(obj) if err != nil { @@ -252,6 +256,17 @@ func (g *Graph) Node(gvk schema.GroupVersionKind, obj metav1.Object) *Node { return node } +// FindNode returns a node by identity when it already exists in the graph. +func (g *Graph) FindNode(apiVersion string, kind string, namespace string, name string) *Node { + for _, node := range g.Nodes { + if node.APIVersion == apiVersion && node.Kind == kind && node.Namespace == namespace && node.Name == name { + return node + } + } + + return nil +} + // Finalize adds missing relationships to the Graph. func (g *Graph) Finalize() error { for _, node := range g.Nodes { From d7a5bc9254da74cab7efb06f3548982fcb3bc9a6 Mon Sep 17 00:00:00 2001 From: Ryszard Szwajlik Date: Mon, 23 Mar 2026 13:56:22 +0100 Subject: [PATCH 2/5] feat: add pod relationships to PersistentVolume and PersistentVolumeClaim --- pkg/graph/core.go | 85 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/pkg/graph/core.go b/pkg/graph/core.go index e280803..192de9b 100644 --- a/pkg/graph/core.go +++ b/pkg/graph/core.go @@ -63,6 +63,18 @@ func (g *CoreV1Graph) Unstructured(unstr *unstructured.Unstructured) (*Node, err return nil, err } return g.Endpoints(obj) + case "PersistentVolume": + obj := &v1.PersistentVolume{} + if err := FromUnstructured(unstr, obj); err != nil { + return nil, err + } + return g.PersistentVolume(obj) + case "PersistentVolumeClaim": + obj := &v1.PersistentVolumeClaim{} + if err := FromUnstructured(unstr, obj); err != nil { + return nil, err + } + return g.PersistentVolumeClaim(obj) case "Service": obj := &v1.Service{} if err := FromUnstructured(unstr, obj); err != nil { @@ -151,6 +163,14 @@ func (g *CoreV1Graph) Pod(pod *v1.Pod) (*Node, error) { g.graph.Relationship(n, "Secret", secret) } + if volume.PersistentVolumeClaim != nil { + pvc, err := g.PersistentVolumeClaimRef(pod.GetNamespace(), volume.PersistentVolumeClaim.ClaimName) + if err != nil { + return nil, err + } + g.graph.Relationship(n, "PersistentVolumeClaim", pvc) + } + if volume.Projected != nil { for _, source := range volume.Projected.Sources { if source.ConfigMap != nil { @@ -462,3 +482,68 @@ func (g *CoreV1Graph) Node(obj *v1.Node) (*Node, error) { return n, nil } + +// PersistentVolume adds a v1.PersistentVolume resource to the Graph. +func (g *CoreV1Graph) PersistentVolume(obj *v1.PersistentVolume) (*Node, error) { + n := g.graph.Node(obj.GroupVersionKind(), obj) + + return n, nil +} + +// PersistentVolumeRef adds a v1.PersistentVolume resource to the Graph or resolves an existing node for it. +func (g *CoreV1Graph) PersistentVolumeRef(name string) (*Node, error) { + if name == "" { + return nil, fmt.Errorf("persistentvolume reference is missing a name") + } + + if n := g.graph.FindNode(v1.SchemeGroupVersion.String(), "PersistentVolume", "", name); n != nil { + return n, nil + } + + n := g.graph.Node( + schema.FromAPIVersionAndKind(v1.GroupName, "PersistentVolume"), + &metav1.ObjectMeta{ + UID: ToUID("PersistentVolume", name), + Name: name, + }, + ) + + return n, nil +} + +// PersistentVolumeClaim adds a v1.PersistentVolumeClaim resource to the Graph. +func (g *CoreV1Graph) PersistentVolumeClaim(obj *v1.PersistentVolumeClaim) (*Node, error) { + n := g.graph.Node(obj.GroupVersionKind(), obj) + + if obj.Spec.VolumeName != "" { + pv, err := g.PersistentVolumeRef(obj.Spec.VolumeName) + if err != nil { + return nil, err + } + g.graph.Relationship(n, "PersistentVolume", pv) + } + + return n, nil +} + +// PersistentVolumeClaimRef adds a v1.PersistentVolumeClaim resource to the Graph or resolves an existing node for it. +func (g *CoreV1Graph) PersistentVolumeClaimRef(namespace string, name string) (*Node, error) { + if name == "" { + return nil, fmt.Errorf("persistentvolumeclaim reference is missing a name") + } + + if n := g.graph.FindNode(v1.SchemeGroupVersion.String(), "PersistentVolumeClaim", namespace, name); n != nil { + return n, nil + } + + n := g.graph.Node( + schema.FromAPIVersionAndKind(v1.GroupName, "PersistentVolumeClaim"), + &metav1.ObjectMeta{ + UID: ToUID("PersistentVolumeClaim", namespace, name), + Namespace: namespace, + Name: name, + }, + ) + + return n, nil +} From e43998ea874a7d480934124a59a5fd27aefcef5f Mon Sep 17 00:00:00 2001 From: Ryszard Szwajlik Date: Mon, 23 Mar 2026 14:00:59 +0100 Subject: [PATCH 3/5] feat: add pod relationships to ServiceAccount --- pkg/graph/core.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pkg/graph/core.go b/pkg/graph/core.go index 192de9b..bc0a6d6 100644 --- a/pkg/graph/core.go +++ b/pkg/graph/core.go @@ -146,6 +146,14 @@ func (g *CoreV1Graph) Pod(pod *v1.Pod) (*Node, error) { g.graph.Relationship(n, "Container", c) } + if pod.Spec.ServiceAccountName != "" { + sa, err := g.ServiceAccount(pod.GetNamespace(), pod.Spec.ServiceAccountName) + if err != nil { + return nil, err + } + g.graph.Relationship(n, "ServiceAccount", sa) + } + for _, volume := range pod.Spec.Volumes { if volume.ConfigMap != nil { cm, err := g.ConfigMap(pod.GetNamespace(), volume.ConfigMap.Name) @@ -405,6 +413,28 @@ func (g *CoreV1Graph) Secret(namespace string, name string) (*Node, error) { return n, nil } +// ServiceAccount adds a v1.ServiceAccount resource to the Graph or resolves an existing node for it. +func (g *CoreV1Graph) ServiceAccount(namespace string, name string) (*Node, error) { + if name == "" { + return nil, fmt.Errorf("serviceaccount reference is missing a name") + } + + if n := g.graph.FindNode(v1.SchemeGroupVersion.String(), "ServiceAccount", namespace, name); n != nil { + return n, nil + } + + n := g.graph.Node( + schema.FromAPIVersionAndKind(v1.GroupName, "ServiceAccount"), + &metav1.ObjectMeta{ + UID: ToUID("ServiceAccount", namespace, name), + Namespace: namespace, + Name: name, + }, + ) + + return n, nil +} + // ServiceTypeClusterIP adds a v1.Service of type ClusterIP to the Graph. func (g *CoreV1Graph) ServiceTypeClusterIP(obj *v1.Service) (*Node, error) { n := g.graph.Node(schema.FromAPIVersionAndKind(v1.GroupName, "Service"), obj) From a78ca54b9af5facd2f2773b4f90374b5da596d08 Mon Sep 17 00:00:00 2001 From: Ryszard Szwajlik Date: Mon, 23 Mar 2026 14:03:00 +0100 Subject: [PATCH 4/5] feat: add pod relationships to ImagePullSecrets --- pkg/graph/core.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/graph/core.go b/pkg/graph/core.go index bc0a6d6..f4edcc5 100644 --- a/pkg/graph/core.go +++ b/pkg/graph/core.go @@ -154,6 +154,14 @@ func (g *CoreV1Graph) Pod(pod *v1.Pod) (*Node, error) { g.graph.Relationship(n, "ServiceAccount", sa) } + for _, imagePullSecret := range pod.Spec.ImagePullSecrets { + secret, err := g.Secret(pod.GetNamespace(), imagePullSecret.Name) + if err != nil { + return nil, err + } + g.graph.Relationship(n, "ImagePullSecret", secret) + } + for _, volume := range pod.Spec.Volumes { if volume.ConfigMap != nil { cm, err := g.ConfigMap(pod.GetNamespace(), volume.ConfigMap.Name) From d72ef5413a13b294eccec457ebf9dccff387ce7e Mon Sep 17 00:00:00 2001 From: Ryszard Szwajlik Date: Mon, 23 Mar 2026 14:07:13 +0100 Subject: [PATCH 5/5] feat: add PV/PVC relationships to StorageClass --- pkg/graph/core.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/pkg/graph/core.go b/pkg/graph/core.go index f4edcc5..91e479d 100644 --- a/pkg/graph/core.go +++ b/pkg/graph/core.go @@ -525,6 +525,14 @@ func (g *CoreV1Graph) Node(obj *v1.Node) (*Node, error) { func (g *CoreV1Graph) PersistentVolume(obj *v1.PersistentVolume) (*Node, error) { n := g.graph.Node(obj.GroupVersionKind(), obj) + if obj.Spec.StorageClassName != "" { + sc, err := g.StorageClass(obj.Spec.StorageClassName) + if err != nil { + return nil, err + } + g.graph.Relationship(n, "StorageClass", sc) + } + return n, nil } @@ -553,6 +561,14 @@ func (g *CoreV1Graph) PersistentVolumeRef(name string) (*Node, error) { func (g *CoreV1Graph) PersistentVolumeClaim(obj *v1.PersistentVolumeClaim) (*Node, error) { n := g.graph.Node(obj.GroupVersionKind(), obj) + if obj.Spec.StorageClassName != nil && *obj.Spec.StorageClassName != "" { + sc, err := g.StorageClass(*obj.Spec.StorageClassName) + if err != nil { + return nil, err + } + g.graph.Relationship(n, "StorageClass", sc) + } + if obj.Spec.VolumeName != "" { pv, err := g.PersistentVolumeRef(obj.Spec.VolumeName) if err != nil { @@ -564,6 +580,27 @@ func (g *CoreV1Graph) PersistentVolumeClaim(obj *v1.PersistentVolumeClaim) (*Nod return n, nil } +// StorageClass adds a storage.k8s.io/v1 StorageClass resource to the Graph or resolves an existing node for it. +func (g *CoreV1Graph) StorageClass(name string) (*Node, error) { + if name == "" { + return nil, fmt.Errorf("storageclass reference is missing a name") + } + + if n := g.graph.FindNode("storage.k8s.io/v1", "StorageClass", "", name); n != nil { + return n, nil + } + + n := g.graph.Node( + schema.FromAPIVersionAndKind("storage.k8s.io/v1", "StorageClass"), + &metav1.ObjectMeta{ + UID: ToUID("StorageClass", name), + Name: name, + }, + ) + + return n, nil +} + // PersistentVolumeClaimRef adds a v1.PersistentVolumeClaim resource to the Graph or resolves an existing node for it. func (g *CoreV1Graph) PersistentVolumeClaimRef(namespace string, name string) (*Node, error) { if name == "" {