[Python] Dependency graph of a kubernetes cluster

Discussão em 'Python' iniciado por Stack, Maio 12, 2025.

  1. Stack

    Stack Membro Participativo

    i'm trying to build a dependency graph of a kubernetes cluster, with resources fetched through APIs, and using networkx to build it, the nodes are captured and well represented, however not all the relations are captured, i don't have previous knowledge of kubernetes but , all the resources are fetched, these are the two functions of fetch and build:

    import os
    import networkx as nx
    from rdflib import Graph as RDFGraph, Namespace, URIRef, Literal, RDF
    from rdflib.namespace import RDFS, RDF, OWL
    from rdflib.plugins.sparql import prepareQuery
    from kubernetes import client,config
    from collections import Counter,defaultdict



    def fetch_and_enrich(api_func, kind, api_version):
    try:
    return [dict(item.to_dict(), kind=kind, apiVersion=api_version) for item in api_func().items]
    except Exception as e:
    print(f"Error fetching {kind}: {e}")
    return []

    def parse_k8s_manifests():

    config.load_kube_config()

    #predefined Endpoints to fetch resources
    v1 = client.CoreV1Api()
    apps_v1 = client.AppsV1Api()
    batch_v1 = client.BatchV1Api()
    networking_v1= client.NetworkingV1Api()
    storage_v1 = client.StorageV1Api()
    autoscaling_v2 = client.AutoscalingV2Api()
    apiextensions_v1 = client.ApiextensionsV1Api()
    rbac_v1 = client.RbacAuthorizationV1Api()
    policy_v1 = client.PolicyV1Api()
    manifests = []

    # CoreV1 resources
    manifests += fetch_and_enrich(v1.list_namespace, "Namespace", "v1")
    manifests += fetch_and_enrich(v1.list_pod_for_all_namespaces, "Pod", "v1")
    manifests += fetch_and_enrich(v1.list_service_for_all_namespaces, "Service", "v1")
    manifests += fetch_and_enrich(v1.list_config_map_for_all_namespaces, "ConfigMap", "v1")
    manifests += fetch_and_enrich(v1.list_secret_for_all_namespaces, "Secret", "v1")
    manifests += fetch_and_enrich(v1.list_node, "Node", "v1")
    manifests += fetch_and_enrich(v1.list_resource_quota_for_all_namespaces, "ResourceQuota", "v1")
    manifests += fetch_and_enrich(v1.list_limit_range_for_all_namespaces, "LimitRange", "v1")

    # AppsV1 resources
    manifests += fetch_and_enrich(apps_v1.list_deployment_for_all_namespaces, "Deployment", "apps/v1")
    manifests += fetch_and_enrich(apps_v1.list_stateful_set_for_all_namespaces, "StatefulSet", "apps/v1")
    manifests += fetch_and_enrich(apps_v1.list_daemon_set_for_all_namespaces, "DaemonSet", "apps/v1")
    manifests += fetch_and_enrich(apps_v1.list_replica_set_for_all_namespaces, "ReplicaSet", "apps/v1")

    #NetworkingV1Api resources
    manifests += fetch_and_enrich(networking_v1.list_ingress_for_all_namespaces, "Ingress", "networking.k8s.io/v1")
    manifests += fetch_and_enrich(networking_v1.list_network_policy_for_all_namespaces, "NetworkPolicy", "networking.k8s.io/v1")

    # BatchV1 resources
    manifests += fetch_and_enrich(batch_v1.list_job_for_all_namespaces, "Job", "batch/v1")
    manifests += fetch_and_enrich(batch_v1.list_cron_job_for_all_namespaces, "CronJob", "batch/v1")

    # Storage
    manifests += fetch_and_enrich(storage_v1.list_storage_class, "StorageClass", "storage.k8s.io/v1")

    # AutoscalingV2
    manifests += fetch_and_enrich(autoscaling_v2.list_horizontal_pod_autoscaler_for_all_namespaces, "HorizontalPodAutoscaler", "autoscaling/v2")

    # APIExtensionsV1
    manifests += fetch_and_enrich(apiextensions_v1.list_custom_resource_definition, "CustomResourceDefinition", "apiextensions.k8s.io/v1")

    # RBAC Authorization
    manifests += fetch_and_enrich(rbac_v1.list_cluster_role, "ClusterRole", "rbac.authorization.k8s.io/v1")
    manifests += fetch_and_enrich(rbac_v1.list_cluster_role_binding, "ClusterRoleBinding", "rbac.authorization.k8s.io/v1")
    manifests += fetch_and_enrich(rbac_v1.list_role_for_all_namespaces, "Role", "rbac.authorization.k8s.io/v1")
    manifests += fetch_and_enrich(rbac_v1.list_role_binding_for_all_namespaces, "RoleBinding", "rbac.authorization.k8s.io/v1")

    # Policy
    manifests += fetch_and_enrich(policy_v1.list_pod_disruption_budget_for_all_namespaces, "PodDisruptionBudget", "policy/v1")



    kinds = Counter(m.get('kind') for m in manifests)
    print("Manifest counts by kind:", kinds)

    return manifests


    def build_dependency_graph(manifests):
    G = nx.DiGraph()

    # Group manifests by kind for fast lookups
    kind_map = defaultdict(list)
    for m in manifests:
    kind_map[m.get('kind')].append(m)

    # Precompute node metadata for each manifest
    node_info = []
    for m in manifests:
    kind = m.get('kind')
    meta = m.get('metadata', {}) or {}
    name = meta.get('name')
    namespace = meta.get('namespace') or 'default'
    if not kind or not name:
    continue
    node_id = f"{kind}:{namespace}:{name}"
    spec = m.get('spec') or {}
    node_info.append((node_id, kind, meta, spec))

    # Add the node with its attributes
    attrs = {'kind': kind, 'name': name, 'namespace': namespace}
    labels = meta.get('labels')
    if isinstance(labels, dict):
    attrs['labels'] = labels
    ann = meta.get('annotations')
    if isinstance(ann, dict):
    attrs['annotations'] = ann
    for k, v in spec.items():
    if isinstance(v, (str, int, float, bool)):
    attrs[f'spec:{k}'] = v
    G.add_node(node_id, **attrs)

    # Now build edges using the precomputed node_info and kind_map
    for node_id, kind, meta, spec in node_info:
    # inNamespace
    namespace = meta.get('namespace') or 'default'
    G.add_edge(node_id, f"Namespace:{namespace}", relation='inNamespace')

    # ownerReferences → owns
    for ref in meta.get('ownerReferences', []):
    owner_id = f"{ref['kind']}:{namespace}:{ref['name']}"
    if G.has_node(owner_id):
    G.add_edge(owner_id, node_id, relation='owns')

    # Per-kind relationships
    if kind == 'Pod':
    # runsOn node
    node_name = spec.get('nodeName')
    if node_name:
    G.add_edge(node_id, f"Node:{node_name}", relation='runsOn')
    # serviceAccount
    sa = spec.get('serviceAccountName')
    if sa:
    G.add_edge(node_id, f"ServiceAccount:{namespace}:{sa}", relation='uses')
    # PVC mounts
    for vol in spec.get('volumes', []) or []:
    pvc = (vol.get('persistentVolumeClaim') or {}).get('claimName')
    if pvc:
    G.add_edge(node_id, f"PersistentVolumeClaim:{namespace}:{pvc}", relation='mounts')

    elif kind == 'Service':
    selector = spec.get('selector') or {}
    if selector:
    for pod in kind_map['Pod']:
    pl = pod.get('metadata', {}).get('labels', {}) or {}
    if all(pl.get(k) == v for k, v in selector.items()):
    G.add_edge(node_id,
    f"Pod:{pod['metadata'].get('namespace','default')}:{pod['metadata']['name']}",
    relation='routes-to')

    elif kind == 'Ingress':
    for rule in spec.get('rules', []) or []:
    for path in (rule.get('http') or {}).get('paths', []) or []:
    svc = (path.get('backend', {}).get('service') or {}).get('name')
    if svc:
    G.add_edge(node_id, f"Service:{namespace}:{svc}", relation='routesTo')

    elif kind == 'Deployment':
    for rs in kind_map['ReplicaSet']:
    for ref in rs.get('metadata', {}).get('ownerReferences', []):
    if ref['kind'] == 'Deployment' and ref['name'] == meta['name']:
    G.add_edge(node_id, f"ReplicaSet:{namespace}:{rs['metadata']['name']}", relation='controls')

    elif kind == 'ReplicaSet':
    for pod in kind_map['Pod']:
    for ref in pod.get('metadata', {}).get('ownerReferences', []):
    if ref['kind'] == 'ReplicaSet' and ref['name'] == meta['name']:
    G.add_edge(node_id, f"Pod:{namespace}:{pod['metadata']['name']}", relation='controls')

    elif kind == 'StatefulSet':
    # controls Pods
    for pod in kind_map['Pod']:
    for ref in pod.get('metadata', {}).get('ownerReferences', []):
    if ref['kind'] == 'StatefulSet' and ref['name'] == meta['name']:
    G.add_edge(node_id, f"Pod:{namespace}:{pod['metadata']['name']}", relation='controls')
    # creates PVCs
    for claim in spec.get('volumeClaimTemplates', []) or []:
    pvc = claim.get('metadata', {}).get('name')
    if pvc:
    G.add_edge(node_id, f"PersistentVolumeClaim:{namespace}:{pvc}", relation='creates')

    elif kind == 'DaemonSet':
    for pod in kind_map['Pod']:
    for ref in pod.get('metadata', {}).get('ownerReferences', []):
    if ref['kind'] == 'DaemonSet' and ref['name'] == meta['name']:
    G.add_edge(node_id, f"Pod:{namespace}:{pod['metadata']['name']}", relation='controls')

    elif kind in ('Job', 'CronJob'):
    if kind == 'CronJob':
    for job in kind_map['Job']:
    for ref in job.get('metadata', {}).get('ownerReferences', []):
    if ref['kind'] == 'CronJob' and ref['name'] == meta['name']:
    G.add_edge(node_id, f"Job:{namespace}:{job['metadata']['name']}", relation='schedules')
    for pod in kind_map['Pod']:
    for ref in pod.get('metadata', {}).get('ownerReferences', []):
    if ref['kind'] == 'Job' and ref['name'] == meta['name']:
    G.add_edge(node_id, f"Pod:{namespace}:{pod['metadata']['name']}", relation='controls')

    elif kind == 'HorizontalPodAutoscaler':
    tr = spec.get('scaleTargetRef', {}) or {}
    tk, tn = tr.get('kind'), tr.get('name')
    if tk and tn:
    G.add_edge(node_id, f"{tk}:{namespace}:{tn}", relation='scales')

    elif kind == 'PersistentVolumeClaim':
    pv = spec.get('volumeName')
    if pv:
    G.add_edge(node_id, f"PersistentVolume:{pv}", relation='binds')

    elif kind == 'PersistentVolume':
    sc = spec.get('storageClassName')
    if sc:
    G.add_edge(node_id, f"StorageClass:{sc}", relation='provisionedBy')

    elif kind == 'NetworkPolicy':
    sel = spec.get('podSelector', {}).get('matchLabels', {}) or {}
    for pod in kind_map['Pod']:
    pl = pod.get('metadata', {}).get('labels', {}) or {}
    if all(pl.get(k) == v for k, v in sel.items()):
    G.add_edge(node_id, f"Pod:{pod['metadata']['namespace']}:{pod['metadata']['name']}", relation='applies-to')

    elif kind == 'PodDisruptionBudget':
    sel = spec.get('selector', {}).get('matchLabels', {}) or {}
    for pod in kind_map['Pod']:
    pl = pod.get('metadata', {}).get('labels', {}) or {}
    if all(pl.get(k) == v for k, v in sel.items()):
    G.add_edge(node_id, f"Pod:{pod['metadata']['namespace']}:{pod['metadata']['name']}", relation='protects')

    elif kind in ('RoleBinding', 'ClusterRoleBinding'):
    rr = m.get('roleRef', {}) or {}
    rk, rn = rr.get('kind'), rr.get('name')
    if rk and rn:
    ns = namespace if rk == 'Role' else ''
    G.add_edge(node_id, f"{rk}:{ns}:{rn}", relation='binds')

    return G


    the captured relations are: applies-to, inNamespace, protects, routes-to, routesTo. so please if anyone could help fix this urgent matter, cuz i already spent a long time trying to fix but this is the closest i could get to what i need.

    Continue reading...

Compartilhe esta Página