Initial version of KubeDiagrams
@@ -1,2 +1,53 @@
|
||||
# KubeDiagrams
|
||||
|
||||
Generate Kubernetes architecture diagrams from Kubernetes manifest files.
|
||||
|
||||
There are several tools to generate Kubernetes architecture diagrams (see **[here](https://github.com/philippemerle/Awesome-Kubernetes-Architecture-Diagrams)**).
|
||||
The main originality of **KubeDiagrams** is its **[configurability](bin/kube-diagrams.yaml)** allowing for instance to deal with custom Kubernetes resources.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* [Python 3.x](https://www.python.org/)
|
||||
* [Diagrams](https://diagrams.mingrammer.com/)
|
||||
```sh
|
||||
$ pip install diagrams
|
||||
```
|
||||
* `dot` command ([Graphviz](https://www.graphviz.org/))
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
$ kube-diagrams -h
|
||||
usage: kube-diagrams [-h] [-o OUTPUT] [-f FORMAT] [-v] filename
|
||||
|
||||
Generate Kubernetes architecture diagrams from Kubernetes manifest files
|
||||
|
||||
positional arguments:
|
||||
filename the Kubernetes manifest filename to process
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-o OUTPUT, --output OUTPUT
|
||||
output diagram filename
|
||||
-f FORMAT, --format FORMAT
|
||||
output format, allowed formats are png (default), jpg, svg, pdf, and dot
|
||||
-c CONFIG, --config CONFIG
|
||||
custom kube-diagrams configuration file
|
||||
-v, --verbose verbosity, set to false by default
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
* [official Kubernetes Cassandra example](examples/cassandra/)
|
||||
* [minikube architecture diagrams](examples/minikube/)
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the GPL-3.0 license - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Todo List
|
||||
|
||||
* Add more examples
|
||||
* Add missed Kubernetes resource types: NetworkPolicy and Endpoint
|
||||
* Make `nodes/*/edges` more declarative
|
||||
* Add diagram node icon for IngressClass
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import copy
|
||||
from diagrams import Diagram, Edge, Cluster
|
||||
from diagrams.aws.enablement import ManagedServices
|
||||
import os
|
||||
import yaml
|
||||
from pprint import pprint
|
||||
|
||||
# Query YAML data.
|
||||
def query_path(data, path, default=None):
|
||||
paths = path.split(".")
|
||||
tmp = data
|
||||
for p in paths[:-1]:
|
||||
tmp = tmp.get(p)
|
||||
if tmp is None:
|
||||
return default
|
||||
return tmp.get(paths[-1], default)
|
||||
|
||||
# Load configuration.
|
||||
config = {}
|
||||
with open(os.path.dirname(__file__) + "/kube-diagrams.yaml") as f:
|
||||
config = yaml.safe_load(f) # load YAML config file
|
||||
|
||||
# Undefined edge
|
||||
UNDEFINED_EDGE = query_path(config, "edges.UNDEFINED", {})
|
||||
|
||||
# Get edge config.
|
||||
def get_edge_config(edge_name):
|
||||
return config.get("edges", {}).get(edge_name, {})
|
||||
|
||||
# Get config associated to a resource.
|
||||
def get_node_config(resource):
|
||||
return config.get("nodes", {}).get(get_type(resource), {})
|
||||
|
||||
# Get resource type, i.e., "kind/apiVersion".
|
||||
def get_type(resource):
|
||||
return resource["kind"] + "/" + resource["apiVersion"]
|
||||
|
||||
# Get the namespace of a resource.
|
||||
# If not defined then returns default namespace.
|
||||
def get_namespace(resource):
|
||||
return query_path(resource, "metadata.namespace", config.get("default_namespace", "default"))
|
||||
|
||||
# Create diagrams node.
|
||||
def create_diagram_node(resource, fontcolor="black"):
|
||||
diagram_node_class = ManagedServices # default diagram node class
|
||||
node_config = get_node_config(resource)
|
||||
if node_config != None: # config found
|
||||
# Get diagram node class name
|
||||
diagram_node_classname = node_config.get("diagram_node_classname")
|
||||
if diagram_node_classname != None: # classname defined
|
||||
# Import Diagrams node class module
|
||||
idx = diagram_node_classname.rfind('.')
|
||||
if idx != -1:
|
||||
exec("import " + diagram_node_classname[:idx])
|
||||
# Get diagram node class
|
||||
diagram_node_class = eval(diagram_node_classname)
|
||||
# Create a diagram node
|
||||
return diagram_node_class(resource["metadata"]["name"], fontcolor=fontcolor)
|
||||
|
||||
class ResourceCluster(object):
|
||||
def __init__(self):
|
||||
self.resources = {} # dict<str, resource>
|
||||
self.clusters = {} # dict<str, Cluster>
|
||||
|
||||
def get_or_create_cluster(self, name):
|
||||
cluster = self.clusters.get(name)
|
||||
if cluster is None:
|
||||
cluster = ResourceCluster()
|
||||
self.clusters[name] = cluster
|
||||
return cluster
|
||||
|
||||
class EdgesContext(list):
|
||||
def __init__(self, resource):
|
||||
list.__init__(self)
|
||||
self.resource = resource
|
||||
self.namespace = get_namespace(resource)
|
||||
|
||||
def add_resource(self, path):
|
||||
target = query_path(self.resource, path)
|
||||
self.append([
|
||||
"%s/%s/%s/%s" % (
|
||||
target["name"],
|
||||
self.namespace,
|
||||
target["kind"],
|
||||
target["apiVersion"],
|
||||
),
|
||||
"REFERENCE"
|
||||
])
|
||||
|
||||
def add_resources(self, path, name_path, namespace_path, kind, apiVersion):
|
||||
resources = query_path(self.resource, path)
|
||||
if resources is None:
|
||||
return
|
||||
for resource in resources:
|
||||
self.append([
|
||||
"%s/%s/%s/%s" % (
|
||||
resource[name_path],
|
||||
resource.get(namespace_path, self.namespace),
|
||||
kind,
|
||||
apiVersion
|
||||
),
|
||||
"REFERENCE"
|
||||
])
|
||||
|
||||
def add_all_resources_matching_labels(self, kind, path, data=None):
|
||||
if data is None:
|
||||
data = self.resource
|
||||
matchLabels = query_path(data, path)
|
||||
if matchLabels is None:
|
||||
return False
|
||||
resourceFound = False
|
||||
for rid, resource in resources.items():
|
||||
if resource["kind"] != kind:
|
||||
continue
|
||||
labels = query_path(resource, "metadata.labels", {})
|
||||
found = True
|
||||
for sk, sv in matchLabels.items():
|
||||
if labels.get(sk) != sv:
|
||||
found = False
|
||||
break
|
||||
if found:
|
||||
self.append([rid, "SELECTOR"])
|
||||
resourceFound = True
|
||||
return resourceFound
|
||||
|
||||
def add_all_volume_resources(self, path):
|
||||
def add_volumes(volumes):
|
||||
for volume in volumes:
|
||||
if "configMap" in volume:
|
||||
self.append([
|
||||
"%s/%s/ConfigMap/v1" % (
|
||||
volume["configMap"]["name"],
|
||||
self.namespace
|
||||
),
|
||||
"REFERENCE"
|
||||
])
|
||||
elif "secret" in volume:
|
||||
self.append([
|
||||
"%s/%s/Secret/v1" % (
|
||||
volume["secret"]["secretName"],
|
||||
self.namespace
|
||||
),
|
||||
"REFERENCE"
|
||||
])
|
||||
elif "persistentVolumeClaim" in volume:
|
||||
self.append([
|
||||
"%s/%s/PersistentVolumeClaim/v1" % (
|
||||
volume["persistentVolumeClaim"]["claimName"],
|
||||
self.namespace
|
||||
),
|
||||
"REFERENCE"
|
||||
])
|
||||
elif "projected" in volume:
|
||||
add_volumes(query_path(volume, "projected.sources"))
|
||||
|
||||
volumes = query_path(resource, path)
|
||||
if volumes != None:
|
||||
add_volumes(volumes)
|
||||
return
|
||||
|
||||
def add_volume_claim_templates(self, path):
|
||||
volumeClaimTemplates = query_path(self.resource, path)
|
||||
if volumeClaimTemplates is None:
|
||||
return
|
||||
for volumeClaimTemplate in volumeClaimTemplates:
|
||||
self.add_storage_class("spec.storageClassName", volumeClaimTemplate)
|
||||
|
||||
def add_storage_class(self, path, data=None):
|
||||
if data is None:
|
||||
data = self.resource
|
||||
storageClassName = query_path(data, path)
|
||||
if storageClassName != None:
|
||||
self.append([
|
||||
"%s/StorageClass/storage.k8s.io/v1" % storageClassName,
|
||||
"REFERENCE"
|
||||
])
|
||||
|
||||
def add_volume(self, path):
|
||||
volumeName = query_path(self.resource, path)
|
||||
if volumeName != None:
|
||||
self.append([
|
||||
"%s/PersistentVolume/v1" % volumeName,
|
||||
"REFERENCE"
|
||||
])
|
||||
|
||||
def add_wait_for_services(self, path):
|
||||
initContainers = query_path(self.resource, path)
|
||||
if initContainers is None:
|
||||
return
|
||||
for ic in initContainers:
|
||||
if ic.get("name") == "wait-for-services":
|
||||
for arg in ic.get("args", []):
|
||||
if arg.startswith("-service="):
|
||||
sn = arg[len("-service="):]
|
||||
self.append([
|
||||
"%s/%s/Service/v1" % (
|
||||
sn,
|
||||
self.namespace
|
||||
),
|
||||
"DEPENDENCE"
|
||||
])
|
||||
|
||||
def add_service_account(self, path):
|
||||
serviceAccountName = query_path(self.resource, path)
|
||||
if serviceAccountName != None:
|
||||
self.append([
|
||||
"%s/%s/ServiceAccount/v1" % (
|
||||
serviceAccountName,
|
||||
self.namespace
|
||||
),
|
||||
"REFERENCE"
|
||||
])
|
||||
|
||||
def add_role(self, path):
|
||||
roleRef = self.resource[path]
|
||||
if self.resource["kind"] == "RoleBinding":
|
||||
namespace = "/%s" % self.namespace
|
||||
else:
|
||||
namespace = ""
|
||||
self.append([
|
||||
"%s%s/%s/%s/v1" % (
|
||||
roleRef["name"],
|
||||
namespace,
|
||||
roleRef["kind"],
|
||||
roleRef.get("apiGroup", "rbac.authorization.k8s.io")
|
||||
),
|
||||
"REFERENCE"
|
||||
])
|
||||
|
||||
def add_subjects(self):
|
||||
for subject in self.resource.get("subjects", []):
|
||||
if subject.get("namespace"):
|
||||
rid = "%s/%s/%s/%s" % (
|
||||
subject["name"],
|
||||
subject["namespace"],
|
||||
subject["kind"],
|
||||
subject.get("apiGroup", "v1")
|
||||
)
|
||||
else:
|
||||
rid = "%s/%s/%s/v1" % (
|
||||
subject["name"],
|
||||
subject["kind"],
|
||||
subject.get("apiGroup", "rbac.authorization.k8s.io")
|
||||
)
|
||||
self.append([rid,"REFERENCE"])
|
||||
|
||||
def add_service(self, path, data=None):
|
||||
if data is None:
|
||||
data = self.resource
|
||||
name = query_path(data, path)
|
||||
if name != None:
|
||||
self.append([
|
||||
"%s/%s/Service/v1" % (
|
||||
name,
|
||||
self.namespace
|
||||
),
|
||||
"REFERENCE"
|
||||
])
|
||||
|
||||
# Parse arguments
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="kube-diagrams",
|
||||
description="Generate Kubernetes architecture diagrams from Kubernetes manifest files")
|
||||
parser.add_argument("filename",
|
||||
help="the Kubernetes manifest filename to process")
|
||||
parser.add_argument("-o", "--output", type=str,
|
||||
help="output diagram filename")
|
||||
parser.add_argument("-f", "--format", type=str,
|
||||
help="output format, allowed formats are png (default), jpg, svg, pdf, and dot",
|
||||
default="png")
|
||||
parser.add_argument("-c", "--config", type=str,
|
||||
help="custom kube-diagrams configuration file")
|
||||
parser.add_argument("-v", "--verbose",
|
||||
help="verbosity, set to false by default",
|
||||
action="store_true", default=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Process arguments.
|
||||
if args.output is None:
|
||||
args.output = args.filename[:args.filename.rfind('.')]
|
||||
|
||||
if args.config != None:
|
||||
with open(args.config) as f:
|
||||
custom_config = yaml.safe_load(f) # load custom config file
|
||||
if "default_namespace" in custom_config:
|
||||
config["default_namespace"] = custom_config["default_namespace"]
|
||||
if "edges" in custom_config:
|
||||
config["edges"].update(custom_config["edges"])
|
||||
if "clusters" in custom_config:
|
||||
config["clusters"].extend(custom_config["clusters"])
|
||||
if "nodes" in custom_config:
|
||||
config["nodes"].update(custom_config["nodes"])
|
||||
|
||||
# Load the Kubernetes manifest file.
|
||||
resources = {} # a map of all resources
|
||||
resource_cluster = ResourceCluster() # Clustering resources
|
||||
print("Load %s..." % args.filename)
|
||||
with open(args.filename) as f:
|
||||
data = yaml.safe_load_all(f) # load YAML file
|
||||
for resource in data:
|
||||
def process_resource(resource):
|
||||
name = resource["metadata"]["name"]
|
||||
if get_node_config(resource).get("scope") == "Cluster":
|
||||
rid = name + "/" + get_type(resource)
|
||||
resources[rid] = resource
|
||||
if resource["kind"] == "Namespace":
|
||||
cluster = resource_cluster.get_or_create_cluster("Namespace: %s" % name)
|
||||
else:
|
||||
cluster = resource_cluster
|
||||
cluster.resources[rid] = resource
|
||||
else: # scope = Namespaced
|
||||
rid = name + "/" + get_namespace(resource) + "/" + get_type(resource)
|
||||
resources[rid] = resource
|
||||
cluster = resource_cluster.get_or_create_cluster("Namespace: %s" % get_namespace(resource))
|
||||
def process_clusters(cluster, resource, clusters):
|
||||
for cluster_config in clusters:
|
||||
data = query_path(resource, cluster_config["path"])
|
||||
if data != None:
|
||||
cluster = cluster.get_or_create_cluster("%s: %s" % (cluster_config["label"], data))
|
||||
clusters.remove(cluster_config)
|
||||
return process_clusters(cluster, resource, clusters)
|
||||
return cluster
|
||||
cluster = process_clusters(
|
||||
cluster,
|
||||
resource,
|
||||
copy.deepcopy(config.get("clusters", []))
|
||||
)
|
||||
cluster.resources[rid] = resource
|
||||
|
||||
if resource["kind"] == "List":
|
||||
for r in resource["items"]:
|
||||
process_resource(r)
|
||||
else:
|
||||
process_resource(resource)
|
||||
|
||||
# Print loaded Kubernetes resources
|
||||
if args.verbose:
|
||||
def process_cluster(cluster, ident=0):
|
||||
for rid, resource in cluster.resources.items():
|
||||
print(" " * ident, "- Resource %s" % rid)
|
||||
for cid, sub_cluster in cluster.clusters.items():
|
||||
print(" " * ident, "- %s" % cid)
|
||||
process_cluster(sub_cluster, ident + 1)
|
||||
print("Loaded Kubernetes resources:")
|
||||
process_cluster(resource_cluster)
|
||||
|
||||
# Generate diagram
|
||||
with Diagram("", filename=args.output, show=False, direction="TB", outformat=args.format):
|
||||
# Generate diagram nodes
|
||||
diagram_nodes = {}
|
||||
def create_nodes(cluster):
|
||||
for rid, resource in cluster.resources.items():
|
||||
diagram_nodes[rid] = create_diagram_node(resource)
|
||||
for cid, sub_cluster in cluster.clusters.items():
|
||||
with Cluster(cid):
|
||||
create_nodes(sub_cluster)
|
||||
create_nodes(resource_cluster)
|
||||
# Generate diagram edges
|
||||
undefined_nodes = {}
|
||||
for resource_id, resource in resources.items():
|
||||
edges = EdgesContext(resource)
|
||||
# execute config code to generate edges
|
||||
code_to_exec = get_node_config(resource).get("edges")
|
||||
if code_to_exec:
|
||||
try:
|
||||
exec(code_to_exec)
|
||||
except Exception as exc:
|
||||
print("Error:", type(exc), ":", exc.args)
|
||||
print("Edges script:\n", code_to_exec)
|
||||
print("Resource:")
|
||||
pprint(resource)
|
||||
# generate diagram edges
|
||||
for edge in edges:
|
||||
edge_rid = edge[0]
|
||||
edge_name = edge[1]
|
||||
try:
|
||||
diagram_nodes[resource_id] >> Edge(**get_edge_config(edge_name)) >> diagram_nodes[edge_rid]
|
||||
except KeyError as ke:
|
||||
print("Error: %s not found!" % ke)
|
||||
tmp = str(edge_rid).split("/")
|
||||
if tmp[2] in ["ServiceAccount", "Service", "ConfigMap"]: #TODO: avoid specific cases
|
||||
rsrc = {
|
||||
"apiVersion": tmp[3],
|
||||
"kind": tmp[2],
|
||||
"metadata": {
|
||||
"name": tmp[1] + "/" + tmp[0]
|
||||
}
|
||||
}
|
||||
else:
|
||||
rsrc = {
|
||||
"apiVersion": tmp[2] + "/" + tmp[3],
|
||||
"kind": tmp[1],
|
||||
"metadata": {
|
||||
"name": tmp[0]
|
||||
}
|
||||
}
|
||||
diagram_node = undefined_nodes.get(edge_rid)
|
||||
if diagram_node is None:
|
||||
diagram_node = create_diagram_node(rsrc, UNDEFINED_EDGE.get("color"))
|
||||
undefined_nodes[edge_rid] = diagram_node
|
||||
diagram_nodes[resource_id] >> Edge(**UNDEFINED_EDGE) >> diagram_node
|
||||
|
||||
print("%s.%s generated." % (args.output, args.format))
|
||||
@@ -0,0 +1,165 @@
|
||||
# Configuration of kube-diagrams
|
||||
default_namespace: default
|
||||
edges:
|
||||
REFERENCE:
|
||||
color: black
|
||||
SELECTOR:
|
||||
color: black
|
||||
style: dashed
|
||||
DEPENDENCE:
|
||||
color: darkgrey
|
||||
UNDEFINED:
|
||||
color: red
|
||||
clusters:
|
||||
- label: K8s Application
|
||||
path: metadata.labels.k8s-app
|
||||
- label: Application
|
||||
path: metadata.labels.app
|
||||
- label: Microservice
|
||||
path: metadata.labels.service
|
||||
nodes:
|
||||
ClusterRole/rbac.authorization.k8s.io/v1:
|
||||
scope: Cluster
|
||||
diagram_node_classname: diagrams.k8s.rbac.CRole
|
||||
edges: |
|
||||
for clusterRoleSelector in query_path(resource, "aggregationRule.clusterRoleSelectors", []):
|
||||
edges.add_all_resources_matching_labels("ClusterRole", "matchLabels", clusterRoleSelector)
|
||||
ClusterRoleBinding/rbac.authorization.k8s.io/v1:
|
||||
scope: Cluster
|
||||
diagram_node_classname: diagrams.k8s.rbac.CRB
|
||||
edges: |
|
||||
edges.add_role("roleRef")
|
||||
edges.add_subjects()
|
||||
CustomResourceDefinition/apiextensions.k8s.io/v1:
|
||||
scope: Cluster
|
||||
diagram_node_classname: diagrams.k8s.others.CRD
|
||||
# no edges
|
||||
ConfigMap/v1:
|
||||
scope: Namespaced
|
||||
diagram_node_classname: diagrams.k8s.podconfig.CM
|
||||
# no edges
|
||||
DaemonSet/apps/v1:
|
||||
scope: Namespaced
|
||||
diagram_node_classname: diagrams.k8s.compute.DS
|
||||
edges: |
|
||||
edges.add_all_resources_matching_labels("Pod", "spec.selector.matchLabels")
|
||||
edges.add_service_account("spec.template.spec.serviceAccountName")
|
||||
edges.add_all_volume_resources("spec.template.spec.volumes")
|
||||
Deployment/apps/v1:
|
||||
scope: Namespaced
|
||||
diagram_node_classname: diagrams.k8s.compute.Deploy
|
||||
edges: |
|
||||
edges.add_all_resources_matching_labels("ReplicaSet", "spec.selector.matchLabels")
|
||||
edges.add_service_account("spec.template.spec.serviceAccountName")
|
||||
edges.add_wait_for_services("spec.template.spec.initContainers")
|
||||
edges.add_all_volume_resources("spec.template.spec.volumes")
|
||||
Group/rbac.authorization.k8s.io/v1:
|
||||
scope: Cluster
|
||||
diagram_node_classname: diagrams.k8s.rbac.Group
|
||||
# no edges
|
||||
HorizontalPodAutoscaler/autoscaling/v2:
|
||||
scope: Namespaced
|
||||
diagram_node_classname: diagrams.k8s.clusterconfig.HPA
|
||||
edges: |
|
||||
edges.add_resource("spec.scaleTargetRef")
|
||||
Ingress/networking.k8s.io/v1:
|
||||
scope: Namespaced
|
||||
diagram_node_classname: diagrams.k8s.network.Ing
|
||||
edges: |
|
||||
for rule in query_path(resource, "spec.rules", []):
|
||||
for path in query_path(rule, "http.paths", []):
|
||||
edges.add_service("backend.service.name", path)
|
||||
IngressClass/networking.k8s.io/v1:
|
||||
scope: Cluster
|
||||
diagram_node_classname: #TODO: to implement
|
||||
# no edges
|
||||
Job/batch/v1:
|
||||
scope: Namespaced
|
||||
diagram_node_classname: diagrams.k8s.compute.Job
|
||||
edges: |
|
||||
edges.add_all_resources_matching_labels("Pod", "spec.selector.matchLabels")
|
||||
edges.add_service_account("spec.template.spec.serviceAccountName")
|
||||
edges.add_all_volume_resources("spec.template.spec.volumes")
|
||||
Namespace/v1:
|
||||
scope: Cluster
|
||||
diagram_node_classname: diagrams.k8s.group.NS
|
||||
# no edges
|
||||
Node/v1:
|
||||
scope: Cluster
|
||||
diagram_node_classname: diagrams.k8s.infra.Node
|
||||
# no edges
|
||||
PersistentVolume/v1:
|
||||
scope: Cluster
|
||||
diagram_node_classname: diagrams.k8s.storage.PV
|
||||
edges: |
|
||||
edges.add_storage_class("spec.storageClassName")
|
||||
PersistentVolumeClaim/v1:
|
||||
scope: Namespaced
|
||||
diagram_node_classname: diagrams.k8s.storage.PVC
|
||||
edges: |
|
||||
edges.add_storage_class("spec.storageClassName")
|
||||
edges.add_volume("spec.volumeName")
|
||||
Pod/v1:
|
||||
scope: Namespaced
|
||||
diagram_node_classname: diagrams.k8s.compute.Pod
|
||||
edges: |
|
||||
edges.add_service_account("spec.serviceAccountName")
|
||||
edges.add_wait_for_services("spec.initContainers")
|
||||
edges.add_all_volume_resources("spec.volumes")
|
||||
ReplicaSet/apps/v1:
|
||||
scope: Namespaced
|
||||
diagram_node_classname: diagrams.k8s.compute.RS
|
||||
edges: |
|
||||
edges.add_all_resources_matching_labels("Pod", "spec.selector.matchLabels")
|
||||
edges.add_service_account("spec.template.spec.serviceAccountName")
|
||||
edges.add_wait_for_services("spec.template.spec.initContainers")
|
||||
edges.add_all_volume_resources("spec.template.spec.volumes")
|
||||
Role/rbac.authorization.k8s.io/v1:
|
||||
scope: Namespaced
|
||||
diagram_node_classname: diagrams.k8s.rbac.Role
|
||||
# no edges
|
||||
RoleBinding/rbac.authorization.k8s.io/v1:
|
||||
scope: Namespaced
|
||||
diagram_node_classname: diagrams.k8s.rbac.RB
|
||||
edges: |
|
||||
edges.add_role("roleRef")
|
||||
edges.add_subjects()
|
||||
Secret/v1:
|
||||
scope: Namespaced
|
||||
diagram_node_classname: diagrams.k8s.podconfig.Secret
|
||||
# no edges
|
||||
Service/v1:
|
||||
scope: Namespaced
|
||||
diagram_node_classname: diagrams.k8s.network.SVC
|
||||
edges: |
|
||||
if not edges.add_all_resources_matching_labels("Pod", "spec.selector"):
|
||||
if not edges.add_all_resources_matching_labels("Deployment", "spec.selector"):
|
||||
edges.add_all_resources_matching_labels("StatefulSet", "spec.selector")
|
||||
ServiceAccount/v1:
|
||||
scope: Namespaced
|
||||
diagram_node_classname: diagrams.k8s.rbac.SA
|
||||
# no edges
|
||||
StatefulSet/apps/v1:
|
||||
scoped: Namespaced
|
||||
diagram_node_classname: diagrams.k8s.compute.STS
|
||||
edges: |
|
||||
edges.add_all_resources_matching_labels("Pod", "spec.selector.matchLabels")
|
||||
edges.add_service_account("spec.template.spec.serviceAccountName")
|
||||
edges.add_service("spec.serviceName")
|
||||
edges.add_wait_for_services("spec.template.spec.initContainers")
|
||||
edges.add_all_volume_resources("spec.template.spec.volumes")
|
||||
edges.add_volume_claim_templates("spec.volumeClaimTemplates")
|
||||
StorageClass/storage.k8s.io/v1:
|
||||
scope: Cluster
|
||||
diagram_node_classname: diagrams.k8s.storage.SC
|
||||
# no edges
|
||||
User/rbac.authorization.k8s.io/v1:
|
||||
scope: Cluster
|
||||
diagram_node_classname: diagrams.k8s.rbac.User
|
||||
# no edges
|
||||
|
||||
#TODO: add other Kubernetes resource types.
|
||||
# Ep diagrams.k8s.network.Ep, Endpoint (alias)
|
||||
# Netpol diagrams.k8s.network.Netpol, NetworkPolicy (alias)
|
||||
# PSP diagrams.k8s.others.PSP
|
||||
# Vol diagrams.k8s.storage.Vol, Volume (alias)
|
||||
@@ -0,0 +1,45 @@
|
||||
# Cassandra Example
|
||||
|
||||
This is based on the [official Kubernetes Cassandra example](https://kubernetes.io/docs/tutorials/stateful-application/cassandra/).
|
||||
|
||||
## Instructions
|
||||
|
||||
Generate the Kubernetes architecture diagram for [cassandra.yml](cassandra.yml):
|
||||
```sh
|
||||
$ kube-diagrams cassandra.yml
|
||||
```
|
||||
|
||||
Start a minikube cluster:
|
||||
```sh
|
||||
$ minikube start --memory 5120 --cpus=4
|
||||
```
|
||||
|
||||
Deploy the Cassandra application:
|
||||
```sh
|
||||
$ kubectl apply -f cassandra.yml
|
||||
```
|
||||
|
||||
Wait a few minutes for the Cassandra application to be deployed.
|
||||
|
||||
Get all Kubernetes resources in the `default` namespace:
|
||||
```sh
|
||||
$ kubectl get all,pvc,serviceaccount,configmap,pv,storageclass -o=yaml > default.yml
|
||||
```
|
||||
|
||||
Generate a Kubernetes architecture diagram for the `default` namespace:
|
||||
```sh
|
||||
$ kube-diagrams default.yml
|
||||
```
|
||||
|
||||
Delete Cassandra application.
|
||||
```sh
|
||||
$ kubectl delete -f cassandra.yml
|
||||
```
|
||||
|
||||
## Generated architecture diagrams
|
||||
|
||||
Architecture diagram for [cassandra.yml](cassandra.yml):
|
||||

|
||||
|
||||
Architecture diagram for a deployed Cassandra instance:
|
||||

|
||||
|
After Width: | Height: | Size: 45 KiB |
@@ -0,0 +1,113 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: cassandra
|
||||
name: cassandra
|
||||
spec:
|
||||
clusterIP: None
|
||||
ports:
|
||||
- port: 9042
|
||||
selector:
|
||||
app: cassandra
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: cassandra
|
||||
labels:
|
||||
app: cassandra
|
||||
spec:
|
||||
serviceName: cassandra
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: cassandra
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: cassandra
|
||||
spec:
|
||||
terminationGracePeriodSeconds: 500
|
||||
containers:
|
||||
- name: cassandra
|
||||
image: gcr.io/google-samples/cassandra:v13
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 7000
|
||||
name: intra-node
|
||||
- containerPort: 7001
|
||||
name: tls-intra-node
|
||||
- containerPort: 7199
|
||||
name: jmx
|
||||
- containerPort: 9042
|
||||
name: cql
|
||||
resources:
|
||||
limits:
|
||||
cpu: "500m"
|
||||
memory: 1Gi
|
||||
requests:
|
||||
cpu: "500m"
|
||||
memory: 512M #CHANGED: instead of 1Gi
|
||||
securityContext:
|
||||
capabilities:
|
||||
add:
|
||||
- IPC_LOCK
|
||||
lifecycle:
|
||||
preStop:
|
||||
exec:
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- nodetool drain
|
||||
env:
|
||||
- name: MAX_HEAP_SIZE
|
||||
value: 512M
|
||||
- name: HEAP_NEWSIZE
|
||||
value: 100M
|
||||
- name: CASSANDRA_SEEDS
|
||||
value: "cassandra-0.cassandra.default.svc.cluster.local"
|
||||
- name: CASSANDRA_CLUSTER_NAME
|
||||
value: "K8Demo"
|
||||
- name: CASSANDRA_DC
|
||||
value: "DC1-K8Demo"
|
||||
- name: CASSANDRA_RACK
|
||||
value: "Rack1-K8Demo"
|
||||
- name: POD_IP
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: status.podIP
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- /bin/bash
|
||||
- -c
|
||||
- echo OK #ERROR: if /ready-probe.sh
|
||||
initialDelaySeconds: 15
|
||||
timeoutSeconds: 5
|
||||
# These volume mounts are persistent. They are like inline claims,
|
||||
# but not exactly because the names need to match exactly one of
|
||||
# the stateful pod volumes.
|
||||
volumeMounts:
|
||||
- name: cassandra-data
|
||||
mountPath: /cassandra_data
|
||||
# These are converted to volume claims by the controller
|
||||
# and mounted at the paths mentioned above.
|
||||
# do not use these in production until ssd GCEPersistentDisk or other ssd pd
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: cassandra-data
|
||||
spec:
|
||||
accessModes: [ "ReadWriteOnce" ]
|
||||
storageClassName: fast
|
||||
resources:
|
||||
requests:
|
||||
storage: 512M #CHANGED: instead of 1Gi
|
||||
---
|
||||
kind: StorageClass
|
||||
apiVersion: storage.k8s.io/v1
|
||||
metadata:
|
||||
name: fast
|
||||
provisioner: k8s.io/minikube-hostpath
|
||||
parameters:
|
||||
type: pd-ssd
|
||||
|
After Width: | Height: | Size: 162 KiB |
@@ -0,0 +1,21 @@
|
||||
#! /bin/sh
|
||||
|
||||
BIN=../../bin
|
||||
|
||||
# Generate the Kubernetes architecture diagram for cassandra.yml.
|
||||
$BIN/kube-diagrams cassandra.yml
|
||||
|
||||
# Deploy the Cassandra application.
|
||||
kubectl apply -f cassandra.yml
|
||||
|
||||
# Wait a few minutes for the Cassandra application to be deployed.
|
||||
sleep 120
|
||||
|
||||
# Get all Kubernetes resources in the default namespace.
|
||||
kubectl get all,pvc,serviceaccount,configmap,pv,storageclass -o=yaml > default.yml
|
||||
|
||||
# Generate a Kubernetes architecture diagram for the default namespace.
|
||||
$BIN/kube-diagrams default.yml
|
||||
|
||||
# Delete Cassandra application.
|
||||
kubectl delete -f cassandra.yml
|
||||
@@ -0,0 +1,38 @@
|
||||
# Minikube Example
|
||||
|
||||
## Instructions
|
||||
|
||||
Start a minikube cluster with `metrics-server`, `dashboard`, and `ingress` addons:
|
||||
|
||||
```sh
|
||||
$ minikube start
|
||||
$ minikube addons enable metrics-server
|
||||
$ minikube addons enable dashboard
|
||||
$ minikube addons enable ingress
|
||||
```
|
||||
|
||||
Generate some Kubernetes architecture diagrams:
|
||||
|
||||
```sh
|
||||
$ ./generate.sh
|
||||
```
|
||||
## Generated architecture diagrams
|
||||
|
||||
Architecture diagram of Kubernetes resources in the minikube `default` namespace:
|
||||

|
||||
|
||||
Architecture diagram of Kubernetes resources in the minikube `kubernetes-dashboard` namespace:
|
||||

|
||||
|
||||
Architecture diagram of all minikube Kubernetes resources except for RBAC ones:
|
||||

|
||||
|
||||
**Note**: [minikube-without-rbac-corrected.yml](minikube-without-rbac-corrected.yml) was manually corrected to better group some Kubernetes resources (see `#ADDED:` and `#CHANGED:` comments).
|
||||
|
||||
Corrected architecture diagram of all minikube Kubernetes resources except for RBAC ones:
|
||||

|
||||
|
||||
Architecture diagram of all minikube Kubernetes resources:
|
||||

|
||||
|
||||
**Note**: In the previous architecture diagram, both red labels and red arrows indicate unfound Kubernetes resources.
|
||||
@@ -0,0 +1,27 @@
|
||||
#! /bin/sh
|
||||
|
||||
BIN=../../bin
|
||||
cluster_name=minikube
|
||||
|
||||
# Get all Kubernetes resources.
|
||||
kubectl get clusterrole,clusterrolebinding,crd,configmap,ds,deployment,hpa,ingress,ingressclass,job,node,pod,replicaset,role,rolebinding,secret,service,serviceaccount,storageclass,namespace -A -o=yaml > ${cluster_name}.yml
|
||||
# Generate Kubernetes architecture diagram.
|
||||
$BIN/kube-diagrams -vv ${cluster_name}.yml
|
||||
|
||||
# Get all Kubernetes resources except for rbac ones.
|
||||
kubectl get crd,configmap,ds,deployment,hpa,ingress,ingressclass,job,node,pod,replicaset,secret,service,serviceaccount,storageclass,namespace -A -o=yaml > ${cluster_name}-without-rbac.yml
|
||||
# Generate Kubernetes architecture diagram.
|
||||
$BIN/kube-diagrams -v ${cluster_name}-without-rbac.yml
|
||||
|
||||
# Generate a Kubernetes architecture diagram from a corrected manifest.
|
||||
$BIN/kube-diagrams -v ${cluster_name}-without-rbac-corrected.yml
|
||||
|
||||
# Get all Kubernetes resources in the default namespace.
|
||||
kubectl get configmap,ds,deployment,hpa,ingress,job,pod,replicaset,role,rolebinding,secret,service,serviceaccount -o=yaml > ${cluster_name}-default.yml
|
||||
# Generate Kubernetes architecture diagram.
|
||||
$BIN/kube-diagrams -v ${cluster_name}-default.yml
|
||||
|
||||
# Get all Kubernetes resources in the kubernetes-dashboard namespace.
|
||||
kubectl get configmap,ds,deployment,hpa,ingress,job,pod,replicaset,role,rolebinding,secret,service,serviceaccount -n kubernetes-dashboard -o=yaml > ${cluster_name}-dashboard.yml
|
||||
# Generate Kubernetes architecture diagram.
|
||||
$BIN/kube-diagrams -v ${cluster_name}-dashboard.yml
|
||||
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,116 @@
|
||||
apiVersion: v1
|
||||
items:
|
||||
- apiVersion: v1
|
||||
data:
|
||||
ca.crt: |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDBjCCAe6gAwIBAgIBATANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwptaW5p
|
||||
a3ViZUNBMB4XDTIzMDIwNDA3NTMzNloXDTMzMDIwMjA3NTMzNlowFTETMBEGA1UE
|
||||
AxMKbWluaWt1YmVDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM3l
|
||||
n8dtEkPntkBRSF7NBlwCbwI3BRxHL5LUSROCfr/tFyuJ8XiZa8yv/DbiCKDFWeBr
|
||||
lJwWOJ3HjrjQc+nBIjbG3spMaOr9Kgut7UKyYEeIZxBSz5GBd7rlRuBwOkRqEXtS
|
||||
/H/TxVzgrugfKbI+ALtzbtHTzKKLDGaYjAC5t2jQ/PtzQ3s4cB8Z/fk03n9woBC8
|
||||
6makLFQXXPj65y09hb/slxIbi4hyRe4hdQYnxeY+PKmuEZIaed5gFrN/EDDtrtwn
|
||||
bcPY3Pwc0/b1+S44TcB+eWMq28zPLeO/Kjziw8q+1wldKWIXJJSUAUU5dBOtlRol
|
||||
yfr7t0ibsft1OTVCisECAwEAAaNhMF8wDgYDVR0PAQH/BAQDAgKkMB0GA1UdJQQW
|
||||
MBQGCCsGAQUFBwMCBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW
|
||||
BBS1Nak8XxDdSOq3r1qdAXNmQRttCzANBgkqhkiG9w0BAQsFAAOCAQEAxrabqVKs
|
||||
u9hJTiUNhLEQTOVR7gMuonymonQ73488pQHKKIyAM8It+ClgP+YdZtRi+uxGgQ3m
|
||||
kwbJbf1ybysY++tIKbUXfsal/wcrt2hbqk6HJxca/YaYL7qja6N/BElct07RQhLf
|
||||
DpMtfqokyBh/yOlobBkrrA+RylGVi1ebWgFM7xtzmuPpeIYurNIcXGpp0d21RuRz
|
||||
0Oxd9g/HHAzZihXbHfPeJJdiQBtPvQ83TZ+T2E0MCkLDUC1okRztR8R5YPG6nZli
|
||||
zVRBh41095iSNFKJhFQyu5Qzme98wdBBQGoxcMOoXk6ioeDQUywHaJMjdCANqubf
|
||||
I700WTtGVr503w==
|
||||
-----END CERTIFICATE-----
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
annotations:
|
||||
kubernetes.io/description: Contains a CA bundle that can be used to verify the
|
||||
kube-apiserver when using internal endpoints such as the internal service
|
||||
IP or kubernetes.default.svc. No other usage is guaranteed across distributions
|
||||
of Kubernetes clusters.
|
||||
creationTimestamp: "2024-12-26T23:24:31Z"
|
||||
managedFields:
|
||||
- apiVersion: v1
|
||||
fieldsType: FieldsV1
|
||||
fieldsV1:
|
||||
f:data:
|
||||
.: {}
|
||||
f:ca.crt: {}
|
||||
f:metadata:
|
||||
f:annotations:
|
||||
.: {}
|
||||
f:kubernetes.io/description: {}
|
||||
manager: kube-controller-manager
|
||||
operation: Update
|
||||
time: "2024-12-26T23:24:31Z"
|
||||
name: kube-root-ca.crt
|
||||
namespace: default
|
||||
resourceVersion: "305"
|
||||
uid: 2cb982e4-6045-41b4-9cf1-c00ac57e8162
|
||||
- apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
creationTimestamp: "2024-12-26T23:24:17Z"
|
||||
labels:
|
||||
component: apiserver
|
||||
provider: kubernetes
|
||||
managedFields:
|
||||
- apiVersion: v1
|
||||
fieldsType: FieldsV1
|
||||
fieldsV1:
|
||||
f:metadata:
|
||||
f:labels:
|
||||
.: {}
|
||||
f:component: {}
|
||||
f:provider: {}
|
||||
f:spec:
|
||||
f:clusterIP: {}
|
||||
f:internalTrafficPolicy: {}
|
||||
f:ipFamilyPolicy: {}
|
||||
f:ports:
|
||||
.: {}
|
||||
k:{"port":443,"protocol":"TCP"}:
|
||||
.: {}
|
||||
f:name: {}
|
||||
f:port: {}
|
||||
f:protocol: {}
|
||||
f:targetPort: {}
|
||||
f:sessionAffinity: {}
|
||||
f:type: {}
|
||||
manager: kube-apiserver
|
||||
operation: Update
|
||||
time: "2024-12-26T23:24:17Z"
|
||||
name: kubernetes
|
||||
namespace: default
|
||||
resourceVersion: "195"
|
||||
uid: b0a63e0d-7e92-4d91-b328-b2f9d73425ed
|
||||
spec:
|
||||
clusterIP: 10.96.0.1
|
||||
clusterIPs:
|
||||
- 10.96.0.1
|
||||
internalTrafficPolicy: Cluster
|
||||
ipFamilies:
|
||||
- IPv4
|
||||
ipFamilyPolicy: SingleStack
|
||||
ports:
|
||||
- name: https
|
||||
port: 443
|
||||
protocol: TCP
|
||||
targetPort: 8443
|
||||
sessionAffinity: None
|
||||
type: ClusterIP
|
||||
status:
|
||||
loadBalancer: {}
|
||||
- apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
creationTimestamp: "2024-12-26T23:24:31Z"
|
||||
name: default
|
||||
namespace: default
|
||||
resourceVersion: "314"
|
||||
uid: 56ce5b99-fb97-40b6-a220-0eff6fd067e5
|
||||
kind: List
|
||||
metadata:
|
||||
resourceVersion: ""
|
||||
selfLink: ""
|
||||
|
After Width: | Height: | Size: 671 KiB |
|
After Width: | Height: | Size: 681 KiB |
|
After Width: | Height: | Size: 1.8 MiB |