Last active
February 1, 2020 14:02
Revisions
-
dvarrazzo renamed this gist
Feb 1, 2020 . 1 changed file with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes. -
dvarrazzo created this gist
Feb 1, 2020 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,650 @@ #!/usr/bin/env python3 """Render a view of the k8s status in a namespace. Example: # Display the state in console k8splore.py -n my-ns -c my-cx --format text # Display the state as a graph, with some nodes omitted. k8splore.py -n my-ns -c my-cx --drop-kind ReplicaSet Pod Job \\ --format dot | dot -Tsvg > my-ns.svg && firefox my-ns.svg """ import re import sys import json import logging import subprocess as sp from operator import attrgetter from collections import defaultdict logger = logging.getLogger() logging.basicConfig( level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s' ) def main(): opt = parse_cmdline() resources = get_all_resources(opt) resources = drop_no_pod(resources) resources = add_containers(resources) resources = add_extnames(resources) resources = create_graph(resources) resources = add_groups(resources) for kind in opt.drop_kind: resources = drop_kind(resources, kind) if opt.format == 'dot': print_dot(resources) elif opt.format == 'text': print_text(resources) else: assert False, f"unknown format {opt.format}" def get_all_resources(opt): kinds = set(['Pod', 'Service']) got = set() resources = {} while kinds: kind = kinds.pop() if kind in got: continue got.add(kind) logger.info("reading resurces of kind %s", kind) cmdline = ["kubectl", "-o", "json"] if opt.context: cmdline += ['--context', opt.context] if opt.namespace: cmdline += ['--namespace', opt.namespace] cmdline += ["get", kind] objs = sp.check_output(cmdline) objs = json.loads(objs)['items'] for obj in objs: obj = Resource(obj) assert obj.id not in resources resources[obj.id] = obj for ref in obj.references: if ref.kind: kinds.add(ref.kind) return resources def drop_no_pod(resources): rv = resources.copy() # delete the replicasets without anything referring to them refs = {ref.id for res in rv.values() for ref in res.references} for res in list(rv.values()): if res.kind == 'ReplicaSet' and res.id not in refs: del rv[res.id] # delete the deployments without anything referring to them refs = {ref.id for res in rv.values() for ref in res.references} for res in list(rv.values()): if res.kind == 'Deployment' and res.id not in refs: del rv[res.id] return rv def add_extnames(resources): rv = resources.copy() for res in resources.values(): if not res.kind == 'Service': continue ext = res.external_name if ext is not None and ext.id not in rv: rv[ext.id] = ext return rv def add_containers(resources): rv = resources.copy() for res in resources.values(): if not res.kind == 'Pod': continue for c in res.containers: if c.id in rv: continue rv[c.id] = c for p in c.ports: assert p.id not in rv, p.id rv[p.id] = p return rv def create_graph(resources): rv = resources.copy() by_label = defaultdict(set) for res in resources.values(): if res.kind != 'Pod': continue for k, v in res.labels.items(): by_label[k, v].add(res.id) for res in resources.values(): if res.kind == 'Service': if res.type == 'ClusterIP': pids = None for sel in res.selector.items(): if pids is None: pids = by_label.get(sel, set()) else: pids &= by_label.get(sel, set()) if pids: for pid in pids: rv[pid].children.add(res.id) else: # No pod offers this service del rv[res.id] elif res.type == 'ExternalName': ext = res.external_name rv[ext.id].children.add(res.id) for ref in res.references: rv[ref.id].children.add(res.id) if res.kind == 'Pod': for c in res.containers: if c.id not in resources: continue res.children.add(c.id) for p in c.ports: if p.id in resources: c.children.add(p.id) return rv def add_groups(resources): def set_recursive(res, group): res.group = group for c in res.children: set_recursive(resources[c], group) for res in resources.values(): if res.kind != 'Deployment': continue if res.name.startswith("api-"): set_recursive(res, 'API') elif res.name.startswith("web-"): set_recursive(res, 'Web') else: set_recursive(res, 'Other') for res in resources.values(): if not hasattr(res, 'group'): res.group = 'Other' return resources def drop_kind(resources, kind): kind = kind.lower() parents = defaultdict(set) for res in resources.values(): for cid in res.children: parents[cid].add(res.id) rv = resources.copy() def del_recursive(id): for c in rv[id].children: del_recursive(c) del rv[id] for res in resources.values(): if res.kind.lower() != kind: continue for pid in parents[res.id]: resources[pid].children.discard(res.id) for cid in res.children: # Don't duplicate containers with the same image if rv[cid].kind == 'Container': iname = rv[cid].image assert len(parents[res.id]) == 1 parent = rv[list(parents[res.id])[0]] for sibid in parent.children: if ( rv[sibid].kind == 'Container' and rv[sibid].image == iname ): rv[sibid].count += 1 del_recursive(cid) break else: parent.children.add(cid) else: for pid in parents[res.id]: resources[pid].children.add(cid) del rv[res.id] return rv def print_dot(resources): groups = defaultdict(list) for res in sorted(resources.values(), key=attrgetter('name')): groups[res.group].append(res) print("digraph g {") print(' rankdir="LR";') print(' node [fontsize=8 fontname=helvetica];') for gname, group in sorted(groups.items(), reverse=True): print(f'subgraph cluster_{gname} {{') print(f'label="{gname}"') for res in group: if res.kind in ('Service', 'Port'): continue if res.kind == 'Container': servs = res.ports else: servs = [ resources[cid] for cid in res.children if resources[cid].kind == 'Service' ] if not servs: node = res.node else: node = res.render_node(label=res.node_label, services=servs) print(f' "{res.id}" {node};') for cid in res.children: if resources[cid].kind in ('Service', 'Port'): continue print(f' "{res.id}" -> "{cid}";') print("}") print("}") def print_text(resources): def key(res): if res.kind in ('Deployment', 'StatefulSet', 'DaemonSet', 'CronJob'): score = 1 elif res.kind in ('Container'): score = 3 elif res.kind in ('Port'): score = 4 else: score = 2 return score, res.kind, res.name groups = defaultdict(list) for res in sorted(resources.values(), key=key): groups[res.group].append(res) printed = set() def print_recursive(res, level=0): if res.id in printed: return # if res.kind == 'Service': # return printed.add(res.id) if res.count > 1: count = f' (x{res.count})' else: count = '' indent = ' ' * level comment = '' if res.kind == 'Deployment': comment = res.comment or '' comment = f' {comment}' print(f"{indent}{res.kind} {res.name}{count}{comment}") if res.kind == 'Service': for port in res.ports: print(f" {indent} {port.name}") children = [resources[c] for c in res.children] children.sort(key=key) for c in children: print_recursive(c, level=level + 1) if level == 0: print() for gname, group in sorted(groups.items()): print(f'group: {gname}\n') for res in group: print_recursive(res) print() class JsonThing: def __init__(self, json): self.json = json self.children = set() self.count = 1 @property def id(self): return f"{self.kind}:{self.name}" @property def kind(self): return self.__class__.__name__ def __repr__(self): return "<%s at 0x%X>" % (self.id, id(self)) def render_node(self, label, shape='box', services=None): def render_port(p): return p.name def render_service(s): rv = [s.name] for port in s.ports: rv.append(port.name) return '\\n'.join(rv) def dot_escstring(s): # https://graphviz.org/doc/info/attrs.html#k:escString # not really, escape only quotes for now return re.sub(r'(["])', r'\\\1', s) if services: shape = 'record' if services[0].kind == 'Service': title = 'Services' services.sort(key=attrgetter('name')) services = ' | '.join(render_service(s) for s in services) else: assert services[0].kind == 'Port', services[0].kind title = 'Ports' services.sort(key=attrgetter('port', 'protocol')) services = ' | '.join(render_port(p) for p in services) label = f'{{ {label} | {{ {title} | {services} }} }}' tooltip = self.comment or '' if tooltip: tooltip = dot_escstring(tooltip) tooltip = f' tooltip="{tooltip}"' return f'[shape={shape} label="{label}"{tooltip}]' @property def node_shape(self): kind = self.kind if kind in ('ExternalName', 'Deployment'): return 'component' elif self.kind == 'Service': return 'oval' elif self.kind == 'Container': return 'box3d' else: return 'box' @property def node_label(self): deets = '' if self.count > 1: count = f' (x{self.count})' else: count = '' if self.kind == 'Service': deets = ', '.join( f"{port.protocol}/{port.port}" for port in self.ports ) deets = f' ({deets})' if self.kind == 'Pod': node = self.json["spec"]["nodeName"].split('.', 1)[0] return f'{self.kind}{count}\\n{node}{deets}' elif self.kind in ('ReplicaSet', 'Job'): return f'{self.kind}{count}' else: return f'{self.kind}{count}\\n{self.name}{deets}' @property def node(self): return self.render_node(shape=self.node_shape, label=self.node_label) @property def references(self): return [] @property def ports(self): if self.kind == 'Service': if not hasattr(self, '_ports'): self._ports = [ Port(p, self) for p in self.json['spec'].get('ports', ()) ] return self._ports else: raise AttributeError(f"you don't find ports on a {self.kind}") @property def comment(self): try: comment = self.json['metadata']['annotations'][ 'kubeonoff/description' ] except (KeyError, TypeError, AttributeError): return else: return comment.splitlines()[0] class Resource(JsonThing): @property def kind(self): return self.json['kind'] @property def type(self): return self.json['spec'].get('type') @property def name(self): return self.json['metadata']['name'] @property def labels(self): return self.json['metadata']['labels'] @property def selector(self): return self.json['spec'].get('selector', {}) @property def references(self): refs = self.json['metadata'].get('ownerReferences', ()) return [Reference(ref) for ref in refs] @property def containers(self): if not hasattr(self, '_containers'): self._containers = [ Container(c, self) for c in self.json['spec'].get('containers', ()) ] return self._containers @property def external_name(self): if ( self.kind == 'Service' and self.json['spec'].get('type') == 'ExternalName' ): return ExternalName(self.json['spec']['externalName']) class Reference(JsonThing): @property def kind(self): return self.json['kind'] @property def name(self): return self.json['name'] class Container(JsonThing): def __init__(self, json, parent): super().__init__(json) self.parent = parent @property def id(self): return f"{self.kind}:{self.parent.name}:{self.name}" @property def name(self): return self.json['name'] @property def image(self): return self.json['image'] @property def node_label(self): image = self.image.rsplit(':', 1)[0] if self.count > 1: count = f' (x{self.count})' else: count = '' return f"{self.kind}: {self.name}{count}\\n{image}" @property def ports(self): if not hasattr(self, '_ports'): self._ports = [Port(p, self) for p in self.json.get('ports', ())] return self._ports class Port(JsonThing): def __init__(self, json, parent): super().__init__(json) self.parent = parent @property def id(self): return ( f"{self.kind}:{self.parent.id}:" + "{protocol}/{containerPort}".format(**self.json) ) @property def name(self): name = self.json.get('name') protocol = self.protocol number = self.port if name is not None: rv = f'{name}: {protocol}/{number}' else: rv = f'{protocol}/{number}' hp = self.json.get('hostPort') if hp is not None: if hp != number: rv = f'{rv} (host: {hp})' else: rv = f'{rv} (host)' return rv @property def protocol(self): return self.json['protocol'] @property def port(self): if 'containerPort' in self.json: # on a container return self.json['containerPort'] else: # on a service return self.json['port'] class ExternalName(JsonThing): @property def id(self): return f"{self.kind}:{self.json}" @property def name(self): return self.json @property def node_label(self): return f"{self.kind}\\n{self.json}" def parse_cmdline(): from argparse import ArgumentParser, RawDescriptionHelpFormatter parser = ArgumentParser( description=__doc__, formatter_class=RawDescriptionHelpFormatter ) parser.add_argument( '--context', '-c', help="kubernetes context to explore" ) parser.add_argument( '--namespace', '-n', help="kubernetes namespace to explore" ) parser.add_argument( '--drop-kind', nargs='*', metavar='KIND', help="remove KIND nodes from the graph", default=[], ) parser.add_argument( '--format', choices=('dot', 'text'), default='dot', help="output format", ) opt = parser.parse_args() return opt if __name__ == '__main__': sys.exit(main())