Skip to content

aalpar/ariadne

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

60 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ariadne

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.

Goals

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.Unstructured objects 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 via spec.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 (like ownerReferences) are generic across all kinds and handled by dedicated code rather than rules.

  • Incremental and reactive. Add/Remove with 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.

Install

go get github.com/aalpar/ariadne

Quick start

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)
}

How it works

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.

Load vs Add

  • Load inserts all nodes first, then resolves edges, then notifies listeners. Use this for initial sync so resolvers can see the full object set.
  • Add resolves per-object as it goes. Use this for incremental watch updates.

Built-in resolvers

NewDefault() registers all three:

Structural

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.

Selector

Label/selector matching:

From To Selector Field
Service Pod spec.selector
NetworkPolicy Pod spec.podSelector
PodDisruptionBudget Pod spec.selector

Event

Creates edges from K8s Events to the objects they describe (via involvedObject). An Event depends on its involved object, not the other way around.

Ecosystem resolvers

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)

Custom resolvers

Declarative rules

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. NamespaceFieldPath specifies where to read the target namespace for cross-namespace references. ClusterScoped marks targets that have no namespace (Node, StorageClass, etc.) — used by Extract to determine the correct namespace without a Lookup. When neither is set, Resolve tries the source's namespace first, then cluster-scoped; Extract inherits the source namespace.
  • LabelSelectorRule — match targets by label selector. The selector format (full matchLabels/matchExpressions vs flat map) is auto-detected.

Resolver interface

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
}
  • Extract inspects the object's fields and returns forward-direction edges without querying the graph. No Lookup is available. Used by ResolveAll for static analysis and linting. Label-selector edges cannot be extracted (they require a Lookup to find matching targets).
  • Resolve returns bidirectional, existence-filtered edges using a read-only Lookup to query existing graph nodes. Resolvers cannot mutate the graph or access edges. Used by Graph.Add and Graph.Load.

Graph API

Query

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 check

Topological operations

sorted, err := g.TopologicalSort()  // dependency order; returns ErrCycle if cycles exist
cycles := g.Cycles()                // find all elementary cycles

Change notifications

g := 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.

Speculative resolution

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 objs

ResolveAll 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.

Export

g.ExportDOT(w)   // Graphviz DOT format
g.ExportJSON(w)   // JSON with nodes and edges arrays

CLI

Install:

go install github.com/aalpar/ariadne/cmd/ariadne@latest

ariadne graph

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 graph

Output formats: dot (default), json.

Flags: -format (output format), -pod-templates (extract synthetic PodTemplates from Deployments, StatefulSets, etc.).

Exit codes: 0 = success, 2 = usage error.

ariadne lint

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 lint

Output 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.

Concurrency

Graph is safe for concurrent use. Reads (DependenciesOf, Upstream, etc.) run concurrently; writes (Add, Remove, Load) are exclusive.

Dependencies

  • k8s.io/apimachinery — unstructured types, schema, label selectors
  • Go standard library

Does not depend on client-go, controller-runtime, or any graph library.

License

Apache License 2.0 — see LICENSE.

About

Go library that builds a directed dependency graph of Kubernetes resources

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages