mirror of
https://github.com/komodorio/helm-dashboard.git
synced 2026-03-24 03:38:04 +00:00
Add a new "Relations" tab after "Images" that visualizes resource dependencies within a Helm release as an interactive force-directed graph. Detects relationships via ownerReferences, *Ref fields, volumes, env refs, service selectors, ingress backends, and RBAC bindings. External resources appear as dashed oval ghost nodes. Color-coded by resource category. Closes #96 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
185 lines
3.6 KiB
Go
185 lines
3.6 KiB
Go
package objects
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
const testManifest = `
|
|
---
|
|
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: my-config
|
|
data:
|
|
key: value
|
|
---
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: my-secret
|
|
type: Opaque
|
|
data:
|
|
password: cGFzcw==
|
|
---
|
|
apiVersion: v1
|
|
kind: ServiceAccount
|
|
metadata:
|
|
name: my-sa
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-app
|
|
spec:
|
|
selector:
|
|
matchLabels:
|
|
app: my-app
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
serviceAccountName: my-sa
|
|
containers:
|
|
- name: main
|
|
image: nginx:latest
|
|
envFrom:
|
|
- configMapRef:
|
|
name: my-config
|
|
env:
|
|
- name: DB_PASS
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: my-secret
|
|
key: password
|
|
volumes:
|
|
- name: config-vol
|
|
configMap:
|
|
name: my-config
|
|
- name: secret-vol
|
|
secret:
|
|
secretName: external-secret
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: my-service
|
|
spec:
|
|
selector:
|
|
app: my-app
|
|
ports:
|
|
- port: 80
|
|
---
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: Ingress
|
|
metadata:
|
|
name: my-ingress
|
|
spec:
|
|
rules:
|
|
- host: example.com
|
|
http:
|
|
paths:
|
|
- path: /
|
|
pathType: Prefix
|
|
backend:
|
|
service:
|
|
name: my-service
|
|
port:
|
|
number: 80
|
|
tls:
|
|
- secretName: tls-cert
|
|
hosts:
|
|
- example.com
|
|
---
|
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
kind: ClusterRoleBinding
|
|
metadata:
|
|
name: my-binding
|
|
roleRef:
|
|
apiGroup: rbac.authorization.k8s.io
|
|
kind: ClusterRole
|
|
name: my-role
|
|
subjects:
|
|
- kind: ServiceAccount
|
|
name: my-sa
|
|
namespace: default
|
|
`
|
|
|
|
func TestExtractRelations(t *testing.T) {
|
|
graph := ExtractRelations(testManifest)
|
|
|
|
// Check nodes
|
|
nodeMap := map[string]RelationNode{}
|
|
for _, n := range graph.Nodes {
|
|
nodeMap[n.ID] = n
|
|
}
|
|
|
|
// In-release nodes
|
|
inReleaseExpected := []string{
|
|
"ConfigMap/my-config",
|
|
"Secret/my-secret",
|
|
"ServiceAccount/my-sa",
|
|
"Deployment/my-app",
|
|
"Service/my-service",
|
|
"Ingress/my-ingress",
|
|
"ClusterRoleBinding/my-binding",
|
|
}
|
|
for _, id := range inReleaseExpected {
|
|
n, ok := nodeMap[id]
|
|
if !ok {
|
|
t.Errorf("missing in-release node %s", id)
|
|
continue
|
|
}
|
|
if !n.InRelease {
|
|
t.Errorf("node %s should be inRelease=true", id)
|
|
}
|
|
}
|
|
|
|
// Ghost nodes (external references)
|
|
ghostExpected := []string{
|
|
"Secret/external-secret",
|
|
"Secret/tls-cert",
|
|
"ClusterRole/my-role",
|
|
}
|
|
for _, id := range ghostExpected {
|
|
n, ok := nodeMap[id]
|
|
if !ok {
|
|
t.Errorf("missing ghost node %s", id)
|
|
continue
|
|
}
|
|
if n.InRelease {
|
|
t.Errorf("node %s should be inRelease=false", id)
|
|
}
|
|
}
|
|
|
|
// Check edges
|
|
edgeSet := map[string]bool{}
|
|
for _, e := range graph.Edges {
|
|
key := e.Source + " -" + e.Type + "-> " + e.Target
|
|
edgeSet[key] = true
|
|
}
|
|
|
|
expectedEdges := []string{
|
|
"Deployment/my-app -volume-> ConfigMap/my-config",
|
|
"Deployment/my-app -volume-> Secret/external-secret",
|
|
"Deployment/my-app -envRef-> ConfigMap/my-config",
|
|
"Deployment/my-app -envRef-> Secret/my-secret",
|
|
"Deployment/my-app -serviceAccount-> ServiceAccount/my-sa",
|
|
"Service/my-service -selector-> Deployment/my-app",
|
|
"Ingress/my-ingress -ingressBackend-> Service/my-service",
|
|
"Ingress/my-ingress -tlsSecret-> Secret/tls-cert",
|
|
"ClusterRoleBinding/my-binding -roleBinding-> ClusterRole/my-role",
|
|
"ClusterRoleBinding/my-binding -roleBinding-> ServiceAccount/my-sa",
|
|
}
|
|
for _, e := range expectedEdges {
|
|
if !edgeSet[e] {
|
|
t.Errorf("missing edge: %s", e)
|
|
}
|
|
}
|
|
|
|
t.Logf("Nodes: %d, Edges: %d", len(graph.Nodes), len(graph.Edges))
|
|
for _, e := range graph.Edges {
|
|
t.Logf(" %s --%s--> %s", e.Source, e.Type, e.Target)
|
|
}
|
|
}
|