Ariadne builds a directed dependency graph of Kubernetes resources. You feed it K8s API objects; it resolves the relationships between them and gives you a queryable, observable graph.
Named after Ariadne of Greek mythology, who gave Theseus the thread to navigate the Labyrinth.
Many tools visualize Kubernetes resource relationships — kubectl-tree, kube-lineage, KubeView, kubectl-graph, Lens Resource Map — but they all connect to a live cluster and own the full pipeline from API discovery to rendering. The relationship knowledge (how a Pod references a ConfigMap, how a Service selects Pods) is hard-coded inside each tool, reimplemented independently, and not reusable.
Ariadne is a library, not a tool. It aims to be the shared foundation that tools embed:
-
Embeddable anywhere. No cluster connection, no informers, no HTTP servers. You provide
unstructured.Unstructuredobjects from any source — client-go, YAML on disk, test fixtures — and get a graph back. This makes it usable in operators, CI pipelines, static analysis, GitOps tools, and custom dashboards. -
Minimal dependency surface. Only
k8s.io/apimachinery. No client-go, no controller-runtime, no graph libraries. Consumers don't inherit a dependency tree that conflicts with their own. -
Declarative, shareable rules. Most resource relationships are expressed as data (
RefRule,LabelSelectorRule), not code. A rule like "Certificate references a Secret viaspec.secretName" is a struct literal, not a function. Rules can be packaged, published, and composed — the same primitives the built-in resolvers use are available to users for CRDs and custom resources. Some relationships (likeownerReferences) are generic across all kinds and handled by dedicated code rather than rules. -
Incremental and reactive.
Add/Removewith change listeners, not just one-shot graph construction. This supports live use in controllers and informer-based systems, not just static analysis.
The long-term bet is that community-contributed rule sets for popular CRDs (cert-manager, Istio, ArgoCD, Crossplane, Prometheus Operator, etc.) become the shared, tool-agnostic registry of how Kubernetes resources relate to each other.
go get github.com/aalpar/ariadne
package main
import (
"fmt"
"os"
"github.com/aalpar/ariadne"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func main() {
// NewDefault registers the built-in structural, selector, and event resolvers.
g := ariadne.NewDefault()
// Feed objects from any source (client-go, informers, YAML, etc.)
// These helpers build unstructured.Unstructured values for the example.
g.Load([]unstructured.Unstructured{
makeConfigMap("default", "app-config"),
makePod("default", "web", "app-config"),
makeService("default", "web-svc", map[string]interface{}{"app": "web"}),
})
// Query: what does the pod depend on?
podRef := ariadne.ObjectRef{Kind: "Pod", Namespace: "default", Name: "web"}
for _, edge := range g.DependenciesOf(podRef) {
fmt.Printf("%s -> %s (%s via %s)\n", edge.From, edge.To, edge.Type, edge.Field)
}
// Transitive: everything upstream of the service
svcRef := ariadne.ObjectRef{Kind: "Service", Namespace: "default", Name: "web-svc"}
for _, ref := range g.Upstream(svcRef) {
fmt.Println(" upstream:", ref)
}
// Export for visualization
g.ExportDOT(os.Stdout)
}When you add objects to the graph, registered resolvers inspect each object and emit edges. Resolvers are bidirectional: adding a Pod discovers that the Pod depends on a ConfigMap, and adding a ConfigMap discovers that existing Pods depend on it.
Add(obj) → for each Resolver → Resolve(obj, lookup) → []Edge → notify listeners
Edges point from dependent to dependency: From depends on To.
Loadinserts all nodes first, then resolves edges, then notifies listeners. Use this for initial sync so resolvers can see the full object set.Addresolves per-object as it goes. Use this for incremental watch updates.
NewDefault() registers all three:
Direct field references between known K8s resource types:
| From | To | Field |
|---|---|---|
| Pod | ServiceAccount | spec.serviceAccountName |
| Pod | ConfigMap | spec.volumes[*].configMap.name |
| Pod | ConfigMap | spec.volumes[*].projected.sources[*].configMap.name |
| Pod | ConfigMap | spec.{,init,ephemeral}Containers[*].envFrom[*].configMapRef.name |
| Pod | ConfigMap | spec.{,init,ephemeral}Containers[*].env[*].valueFrom.configMapKeyRef.name |
| Pod | Secret | spec.imagePullSecrets[*].name |
| Pod | Secret | spec.volumes[*].secret.secretName |
| Pod | Secret | spec.volumes[*].projected.sources[*].secret.name |
| Pod | Secret | spec.{,init,ephemeral}Containers[*].envFrom[*].secretRef.name |
| Pod | Secret | spec.{,init,ephemeral}Containers[*].env[*].valueFrom.secretKeyRef.name |
| Pod | PersistentVolumeClaim | spec.volumes[*].persistentVolumeClaim.claimName |
| Pod | Node | spec.nodeName |
| Pod | PriorityClass | spec.priorityClassName |
| Pod | RuntimeClass | spec.runtimeClassName |
| PVC | PersistentVolume | spec.volumeName |
| PVC | StorageClass | spec.storageClassName |
| Ingress | Service | spec.rules[*].http.paths[*].backend.service.name |
| Ingress | Service | spec.defaultBackend.service.name |
| Ingress | Secret | spec.tls[*].secretName |
| Ingress | IngressClass | spec.ingressClassName |
| StatefulSet | Service | spec.serviceName |
| PV | StorageClass | spec.storageClassName |
| PV | PersistentVolumeClaim | spec.claimRef (typed-ref) |
| HPA | scaleTargetRef kind | spec.scaleTargetRef (typed-ref) |
| RoleBinding | Role/ClusterRole | roleRef (typed-ref) |
| RoleBinding | ServiceAccount | subjects[*] (typed-ref) |
| ClusterRoleBinding | ClusterRole | roleRef (typed-ref) |
| ClusterRoleBinding | ServiceAccount | subjects[*] (typed-ref) |
| any | owner | metadata.ownerReferences |
All Pod rules are mirrored for PodTemplate (with field paths prefixed by
template.). Use WithPodTemplates() to automatically extract synthetic
PodTemplates from Deployments, StatefulSets, DaemonSets, ReplicaSets, Jobs,
and CronJobs — this lets the resolver trace dependencies through embedded pod
specs without requiring standalone Pod objects.
Label/selector matching:
| From | To | Selector Field |
|---|---|---|
| Service | Pod | spec.selector |
| NetworkPolicy | Pod | spec.podSelector |
| PodDisruptionBudget | Pod | spec.selector |
Creates edges from K8s Events to the objects they describe (via
involvedObject). An Event depends on its involved object, not the
other way around.
Opt-in resolvers for popular CRD ecosystems. Register them with WithResolver():
g := ariadne.NewDefault(
ariadne.WithResolver(ariadne.NewArgoCDResolver()),
ariadne.WithResolver(ariadne.NewGatewayAPIResolver()),
// ...
)| Resolver | Edges |
|---|---|
NewArgoCDResolver() |
Application→Namespace (destination), Application→AppProject |
NewCrossplaneResolver(managed...) |
Managed resources→ProviderConfig, Composition→Composite instances |
NewKyvernoResolver() |
ClusterPolicy/Policy→matched resource kinds |
NewGatewayAPIResolver() |
HTTPRoute→Service (backendRef), HTTPRoute→Gateway (parentRef), Gateway→GatewayClass |
NewClusterAPIResolver() |
Machine/Cluster/MachineDeployment→infrastructure and bootstrap providers (typed-refs) |
For CRDs and custom resources, compose rules from the same primitives the built-in resolvers use:
g := ariadne.New(
ariadne.WithResolver(ariadne.NewRuleResolver("my-app",
ariadne.RefRule{
FromGroup: "db.example.com", FromKind: "DatabaseCluster",
ToKind: "Secret",
FieldPath: "spec.credentialsSecretName",
},
ariadne.RefRule{
FromGroup: "gateway.example.com", FromKind: "HTTPRoute",
ToKind: "Service",
FieldPath: "spec.backendRefs[*].name",
NamespaceFieldPath: "spec.backendRefs[*].namespace",
},
ariadne.LabelSelectorRule{
FromGroup: "app.example.com", FromKind: "Canary",
ToKind: "Pod",
SelectorFieldPath: "spec.selector.matchLabels",
},
)),
)Rule types:
RefRule— a field contains the name of a target resource.NamespaceFieldPathspecifies where to read the target namespace for cross-namespace references.ClusterScopedmarks targets that have no namespace (Node, StorageClass, etc.) — used byExtractto determine the correct namespace without a Lookup. When neither is set,Resolvetries the source's namespace first, then cluster-scoped;Extractinherits the source namespace.LabelSelectorRule— match targets by label selector. The selector format (fullmatchLabels/matchExpressionsvs flat map) is auto-detected.
For logic beyond declarative rules, implement the interface directly:
type Resolver interface {
Name() string
Extract(obj *unstructured.Unstructured) []Edge
Resolve(obj *unstructured.Unstructured, lookup Lookup) []Edge
}Extractinspects the object's fields and returns forward-direction edges without querying the graph. No Lookup is available. Used byResolveAllfor static analysis and linting. Label-selector edges cannot be extracted (they require a Lookup to find matching targets).Resolvereturns bidirectional, existence-filtered edges using a read-onlyLookupto query existing graph nodes. Resolvers cannot mutate the graph or access edges. Used byGraph.AddandGraph.Load.
g.Get(ref) // retrieve stored *unstructured.Unstructured
g.DependenciesOf(ref) // direct outgoing edges (what ref depends on)
g.DependentsOf(ref) // direct incoming edges (what depends on ref)
g.Upstream(ref) // transitive closure of DependenciesOf
g.Downstream(ref) // transitive closure of DependentsOf
g.Nodes() // all ObjectRefs
g.Edges() // all Edges
g.Has(ref) // membership checksorted, err := g.TopologicalSort() // dependency order; returns ErrCycle if cycles exist
cycles := g.Cycles() // find all elementary cyclesg := ariadne.NewDefault(
ariadne.WithListener(func(event ariadne.GraphEvent) {
// event.Type: NodeAdded, NodeRemoved, EdgeAdded, EdgeRemoved
// event.Ref: set for node events
// event.Edge: set for edge events
}),
)Listeners fire synchronously under the write lock. Dispatch expensive work to a goroutine.
Graph.Load only creates edges between objects that both exist. To discover
references to missing targets (dangling references), use ResolveAll:
edges := ariadne.ResolveAll(objs,
ariadne.NewStructuralResolver(),
ariadne.NewSelectorResolver(),
)
// edges includes references to targets not in objsResolveAll calls Extract (not Resolve) on each resolver — it extracts
forward-direction references from field inspection alone, without a Lookup.
Label-selector and reverse edges are not included. The returned edges are
deduplicated.
This is useful for linting, static analysis, and any scenario where you need to know what an object would reference regardless of what exists in the graph.
g.ExportDOT(w) // Graphviz DOT format
g.ExportJSON(w) // JSON with nodes and edges arraysInstall:
go install github.com/aalpar/ariadne/cmd/ariadne@latest
Reads Kubernetes YAML manifests and outputs the full dependency graph.
# DOT output (default) — pipe to Graphviz
ariadne graph manifests/ | dot -Tpng -o graph.png
# JSON output
ariadne graph -format json manifests/
# Extract PodTemplates from workloads for finer-grained edges
ariadne graph -pod-templates manifests/
# Pipe from template tools
helm template my-chart | ariadne graphOutput formats: dot (default), json.
Flags: -format (output format), -pod-templates (extract synthetic
PodTemplates from Deployments, StatefulSets, etc.).
Exit codes: 0 = success, 2 = usage error.
Reads Kubernetes YAML manifests and reports dangling references — resources that are referenced but not present in the input set.
# Pipe from kustomize, helm, or other template tools
kustomize build . | ariadne lint
helm template my-chart | ariadne lint
# Read files and directories (recursive, *.yaml and *.yml)
ariadne lint manifests/ extra.yaml
# Stdin when no args
cat deployment.yaml | ariadne lintOutput is one line per finding:
core/Pod/default/web -> core/Secret/default/app-secret (spec.volumes[*].secret.secretName): not found
Exit codes: 0 = clean, 1 = dangling references found, 2 = usage error.
The linter uses ResolveAll with all built-in and ecosystem resolvers. It
filters out ownerReferences (set by controllers at runtime, not by manifest
authors) and event edges.
Graph is safe for concurrent use. Reads (DependenciesOf, Upstream, etc.)
run concurrently; writes (Add, Remove, Load) are exclusive.
k8s.io/apimachinery— unstructured types, schema, label selectors- Go standard library
Does not depend on client-go, controller-runtime, or any graph library.
Apache License 2.0 — see LICENSE.