├── tests ├── k8s │ ├── __init__.py │ ├── apps │ │ ├── __init__.py │ │ └── test_v1.py │ ├── core │ │ ├── __init__.py │ │ └── test_v1.py │ └── networking │ │ ├── __init__.py │ │ └── test_v1.py └── __init__.py ├── src ├── k8s │ ├── apps │ │ ├── __init__.py │ │ └── v1.py │ ├── core │ │ ├── __init__.py │ │ └── v1.py │ ├── networking │ │ ├── __init__.py │ │ └── v1.py │ ├── watcher.py │ └── __init__.py ├── salmorejo │ ├── __init__.py │ ├── __main__.py │ └── service.py ├── config │ ├── __init__.py │ ├── envs.py │ └── logger.py └── cli │ ├── __init__.py │ └── main.py ├── requirements.txt ├── setup.py ├── requirements-dev.txt ├── environments └── kind │ ├── Makefile │ ├── kind.yaml │ └── README.md ├── scripts ├── print_object.py ├── README.md ├── print_metadata.py ├── counter.py └── dashboard.py ├── venv_creation.sh ├── .github ├── ISSUE_TEMPLATE │ └── feature_request.md ├── FUNDING.yml └── workflows │ ├── unittest.yaml │ └── pythonpublish.yaml ├── .gitignore ├── pyproject.toml ├── LICENSE └── README.md /tests/k8s/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/k8s/apps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/k8s/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/salmorejo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/k8s/apps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/k8s/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/k8s/networking/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/k8s/networking/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/config/__init__.py: -------------------------------------------------------------------------------- 1 | import config.logger -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==8.1.3 2 | kubernetes==24.2.0 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | beautifultable==1.1.0 2 | plotly==5.10.0 3 | ipywidgets>=7.0.0 4 | dash==2.6.2 5 | dash-bootstrap-components==1.2.1 -------------------------------------------------------------------------------- /src/salmorejo/__main__.py: -------------------------------------------------------------------------------- 1 | from cli.main import root 2 | 3 | def main(): 4 | root() 5 | 6 | if __name__ == "__main__": 7 | main() -------------------------------------------------------------------------------- /environments/kind/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: kind 2 | 3 | kind: 4 | kind create cluster --config kind.yaml 5 | 6 | destroy: 7 | kind delete cluster --name development 8 | -------------------------------------------------------------------------------- /environments/kind/kind.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | name: development 4 | networking: {} 5 | nodes: 6 | - role: control-plane 7 | image: kindest/node:v1.23.6 8 | -------------------------------------------------------------------------------- /environments/kind/README.md: -------------------------------------------------------------------------------- 1 | # Kind 2 | 3 | ## Create Kubernetes cluster 4 | 5 | ```bash 6 | $ make kind 7 | ``` 8 | 9 | ## Destroy Kubernetes cluster 10 | 11 | ```bash 12 | $ make destroy 13 | ``` 14 | -------------------------------------------------------------------------------- /scripts/print_object.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | def callback(event): 4 | print(f"\n---------- {event['raw_object']['kind']} -------------") 5 | print(json.dumps(event['raw_object'], sort_keys=True, indent=4)) 6 | print("--------------------------------------\n") -------------------------------------------------------------------------------- /venv_creation.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ -f "venv" ]] 4 | then 5 | rm -rf venv 6 | fi 7 | 8 | virtualenv -p /usr/bin/python3 venv 9 | venv/bin/python -m pip install --upgrade pip 10 | venv/bin/pip install -r requirements.txt 11 | venv/bin/pip install -r requirements-dev.txt 12 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This folder contains some examples about simple scripts that you can work with. 4 | 5 | ## Printing Objects 6 | 7 | ```bash 8 | $ watch ./print_object pods,services 9 | ``` 10 | 11 | ## Printing Object's Metadata 12 | 13 | ```bash 14 | $ watch ./print_metadata pods,services 15 | ``` 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement, help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe what you need** 11 | 14 | 15 | **Proposed solution** 16 | 19 | -------------------------------------------------------------------------------- /src/config/envs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | _LOGGER_LEVEL_NAME="LOGGER_LEVEL" 5 | 6 | _DEFAULT_LOGGER_LEVEL=logging._levelToName[logging.INFO] 7 | 8 | _logger_level=None 9 | 10 | def get_logger_level(): 11 | global _logger_level 12 | if _logger_level is None: 13 | _logger_level = logging._nameToLevel[os.getenv(_LOGGER_LEVEL_NAME, _DEFAULT_LOGGER_LEVEL)] 14 | return _logger_level -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | class TestCaseWatcher(unittest.TestCase): 4 | def check_args(self, actual, expected): 5 | self.assertEqual(len(actual), len(expected)) 6 | for i in range(len(actual)): 7 | if callable(actual[i]) and callable(expected[i]): 8 | self.assertEqual(actual[i].__code__.co_code, expected[i].__code__.co_code) 9 | else: 10 | self.assertEqual(actual[i], expected[i]) -------------------------------------------------------------------------------- /scripts/print_metadata.py: -------------------------------------------------------------------------------- 1 | from beautifultable import BeautifulTable 2 | 3 | table = BeautifulTable() 4 | table.columns.header = ["KIND", "NAMESPACE", "NAME", "PHASE"] 5 | 6 | def callback(event): 7 | phase = event['raw_object']['status'].get('phase') if 'phase' in event['raw_object']['status'] else "" 8 | table.rows.append([ 9 | event['raw_object']['kind'], 10 | event['raw_object']['metadata']['namespace'], 11 | event['raw_object']['metadata']['name'], 12 | phase 13 | ]) 14 | print("\n*****\n") 15 | print(table) 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: mendrugory 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/unittest.yaml: -------------------------------------------------------------------------------- 1 | name: Unit Testing 2 | 3 | on: 4 | push: 5 | pull_request: 6 | types: [opened] 7 | 8 | jobs: 9 | tests: 10 | name: "Python ${{ matrix.python-version }}" 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ["3.8", "3.9", "3.10"] 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: "actions/setup-python@v2" 18 | with: 19 | python-version: "${{ matrix.python-version }}" 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install -r requirements.txt 24 | - name: Run tests 25 | env: 26 | PYTHONPATH: ./src 27 | run: python -m unittest discover . -------------------------------------------------------------------------------- /src/config/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from config.envs import get_logger_level 4 | 5 | _DEFAULT_FORMAT='[%(name)s]: %(message)s' 6 | _LOGGER_NAME='salmorejo' 7 | 8 | _logger = None 9 | 10 | def get_logger(): 11 | global _logger 12 | if _logger is None: 13 | _logger = _get_custom_logger(_LOGGER_NAME, level=get_logger_level()) 14 | return _logger 15 | 16 | def _get_custom_logger(logger_name, format=_DEFAULT_FORMAT, level=logging.INFO): 17 | logger_format = logging.Formatter(format) 18 | logger_handler = logging.StreamHandler(sys.stdout) 19 | logger_handler.setFormatter(logger_format) 20 | logger_handler.setLevel(level) 21 | logger = logging.getLogger(logger_name) 22 | logger.addHandler(logger_handler) 23 | logger.setLevel(level) 24 | return logger -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | app.py 52 | # Sphinx documentation 53 | doc/build/ 54 | 55 | # Others 56 | venv/ 57 | .vscode 58 | pypi 59 | -------------------------------------------------------------------------------- /src/k8s/networking/v1.py: -------------------------------------------------------------------------------- 1 | from kubernetes import client 2 | from k8s import Watcher 3 | 4 | _ingress_watcher = None 5 | 6 | def get_ingress_watcher(namespace=None): 7 | global _ingress_watcher 8 | if _ingress_watcher is None: 9 | _ingress_watcher = IngressWatcher(namespace=namespace) 10 | return _ingress_watcher 11 | 12 | 13 | class IngressWatcher(Watcher): 14 | def __init__(self, namespace=None, auto_start=False): 15 | self.namespace=namespace 16 | super().__init__(auto_start) 17 | 18 | def get_stream_args(self): 19 | if self.namespace is None: 20 | return [client.NetworkingV1Api().list_ingress_for_all_namespaces] 21 | 22 | return [client.NetworkingV1Api().list_namespaced_ingress, self.namespace] 23 | 24 | def get_stream_kwargs(self): 25 | return self._get_stream_default_kwargs() 26 | -------------------------------------------------------------------------------- /.github/workflows/pythonpublish.yaml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: Set up Python 14 | uses: actions/setup-python@v1 15 | with: 16 | python-version: '3.x' 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install setuptools wheel twine build virtualenv 21 | - name: Build and publish 22 | env: 23 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 24 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 25 | run: | 26 | echo "Deploying version ${{ github.ref }}" 27 | sed -i "s|RELEASE_VERSION|${GITHUB_REF/refs\/tags\//}|g" pyproject.toml 28 | python -m build 29 | twine upload dist/* 30 | -------------------------------------------------------------------------------- /src/cli/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.machinery import SourceFileLoader 2 | from k8s.watcher import is_object_allowed 3 | 4 | def get_callback_from_script(path): 5 | module = SourceFileLoader("module", path).load_module() 6 | return module.callback 7 | 8 | def get_set_k8s_objects_from_str(objects_str): 9 | objects = objects_str.replace(" ", "").split(",") 10 | non_allowed_objects = tuple((o for o in objects if not is_object_allowed(o))) 11 | 12 | if non_allowed_objects: 13 | error_message = _get_non_allowed_objects_error_message(non_allowed_objects) 14 | raise Exception(error_message) 15 | 16 | return set(objects) 17 | 18 | def _get_non_allowed_objects_error_message(non_allowed_objects): 19 | if len(non_allowed_objects) == 1: 20 | return f"{non_allowed_objects[0]} is not supported" 21 | 22 | return f"{','.join(non_allowed_objects)} are not supported" -------------------------------------------------------------------------------- /src/salmorejo/service.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from k8s import load_kubeconfig 3 | from k8s.watcher import get_watcher 4 | from config.logger import get_logger 5 | 6 | logger = get_logger() 7 | 8 | class Manager(): 9 | def __init__(self, callback, k8s_objects): 10 | self.service = Service(callback) 11 | self.watchers = set(get_watcher(o) for o in k8s_objects) 12 | 13 | def start(self): 14 | logger.debug(f"There is {len(self.watchers)} watchers.") 15 | load_kubeconfig() 16 | for w in self.watchers: 17 | w.register(self.service) 18 | w.start() 19 | for w in self.watchers: 20 | w.join() 21 | 22 | class Service(): 23 | def __init__(self, callback): 24 | self.callback = callback 25 | 26 | def notify(self, event): 27 | try: 28 | self.callback(event) 29 | except Exception as e: 30 | logger.error(f"callback error: {e}") 31 | -------------------------------------------------------------------------------- /tests/k8s/networking/test_v1.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from kubernetes import client 3 | from k8s.networking.v1 import get_ingress_watcher, IngressWatcher 4 | from tests import TestCaseWatcher 5 | 6 | class IngressWatcherTest(TestCaseWatcher): 7 | def test_args_without_namespace(self): 8 | actual = IngressWatcher().get_stream_args() 9 | expected = [client.NetworkingV1Api().list_ingress_for_all_namespaces] 10 | self.check_args(actual, expected) 11 | 12 | def test_args_with_namespace(self): 13 | namespace = "test" 14 | actual = IngressWatcher(namespace).get_stream_args() 15 | expected = [ 16 | client.NetworkingV1Api().list_namespaced_ingress, 17 | namespace 18 | ] 19 | self.check_args(actual, expected) 20 | 21 | def test_get_ingress_watcher(self): 22 | watcher = get_ingress_watcher() 23 | self.assertTrue(watcher) 24 | 25 | 26 | if __name__ == '__main__': 27 | unittest.main() -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "salmorejo" 7 | version = "RELEASE_VERSION" 8 | authors = [{name = "Gonzalo Gabriel Jiménez Fuentes", email = "iam@mendrugory.com"}] 9 | description = "Tool for testing and debugging Kubernetes events/changes in real time" 10 | license = { file = "LICENSE" } 11 | keywords = ["salmorejo", "kubernetes", "watcher"] 12 | dependencies = [ 13 | 'click == 8.1.3', 14 | 'kubernetes == 24.2.0', 15 | ] 16 | classifiers = [ 17 | "License :: OSI Approved :: MIT License", 18 | "Programming Language :: Python", 19 | "Programming Language :: Python :: 3", 20 | ] 21 | requires-python = ">=3.8" 22 | readme = "README.md" 23 | 24 | [project.optional-dependencies] 25 | dev = ["beautifultable==1.1.0"] 26 | 27 | [project.urls] 28 | Homepage = "https://github.com/mendrugory/salmorejo" 29 | 30 | [project.scripts] 31 | salmorejo = "salmorejo.__main__:main" -------------------------------------------------------------------------------- /src/cli/main.py: -------------------------------------------------------------------------------- 1 | import click 2 | from config.logger import get_logger 3 | from salmorejo.service import Manager 4 | from . import get_callback_from_script, get_set_k8s_objects_from_str 5 | 6 | logger = get_logger() 7 | 8 | @click.group() 9 | def root(): 10 | ''' 11 | Simple CLI 12 | ''' 13 | print("\nSalmorejo is on its way !!") 14 | 15 | @root.command(name='watch', help='watching objects after providing the path of the python script which contains the function `callback(object)` and the comma-separated-string with the desired kubernetes objects (i.e. `watch /home/scripts/my_script.py pods,services)') 16 | @click.argument('callback_path', type=click.Path(exists=True)) 17 | @click.argument('k8s_objects') 18 | def watch(callback_path, k8s_objects): 19 | try: 20 | callback = get_callback_from_script(callback_path) 21 | objects = get_set_k8s_objects_from_str(k8s_objects) 22 | Manager(callback, objects).start() 23 | except Exception as e: 24 | logger.error(f"error: {e}") 25 | -------------------------------------------------------------------------------- /scripts/counter.py: -------------------------------------------------------------------------------- 1 | import os 2 | import threading 3 | from beautifultable import BeautifulTable 4 | 5 | counters = dict() 6 | mutex = threading.Lock() 7 | 8 | _separator = "***" 9 | 10 | def callback(event): 11 | global counters 12 | global mutex 13 | namespaced_name = f"{event['raw_object']['metadata']['namespace']}{_separator}{event['raw_object']['kind']}" 14 | mutex.acquire() 15 | current_count = counters.get(namespaced_name, 0) 16 | if event['type'] == 'ADDED': 17 | counters[namespaced_name] = current_count + 1 18 | if event['type'] == 'DELETED': 19 | counters[namespaced_name] = max(current_count - 1, 0) 20 | print_counters() 21 | mutex.release() 22 | 23 | 24 | def print_counters(): 25 | global counters 26 | table = BeautifulTable() 27 | table.columns.header = ["KIND", "NAMESPACE", "COUNT"] 28 | for key, count in counters.items(): 29 | namespace, kind = key.split(_separator) 30 | table.rows.append([kind, namespace, count]) 31 | os.system("clear") 32 | print(table) 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Gonzalo Gabriel Jiménez Fuentes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/k8s/watcher.py: -------------------------------------------------------------------------------- 1 | from k8s.apps.v1 import get_daemonset_watcher, get_deployment_watcher, get_replicaset_watcher, get_statefulset_watcher 2 | from k8s.core.v1 import get_configmap_watcher, get_pod_watcher, get_secret_watcher, get_service_watcher 3 | from k8s.networking.v1 import get_ingress_watcher 4 | 5 | KUBERNETES_CONFIGMAPS_NAMES=("configmap", "configmaps", "cm",) 6 | KUBERNETES_DAEMONSETS_NAMES=("daemonset", "daemonsets", "ds",) 7 | KUBERNETES_DEPLOYMENTS_NAMES=("deployments", "deployment",) 8 | KUBERNETES_INGRESSES_NAMES=("ingress", "ingresses", "ing",) 9 | KUBERNETES_PODS_NAMES=("pods", "pod", "po",) 10 | KUBERNETES_REPLICASETS_NAMES=("replicaset", "replicasets", "rs",) 11 | KUBERNETES_SECRETS_NAMES=("secret", "secrets",) 12 | KUBERNETES_SERVICES_NAMES=("services", "service", "svc",) 13 | KUBERNETES_STATEFULSETS_NAMES=("statefulset", "statefulsets", "sts",) 14 | 15 | ALLOWED_K8S_OBJECTS = KUBERNETES_CONFIGMAPS_NAMES \ 16 | + KUBERNETES_DAEMONSETS_NAMES \ 17 | + KUBERNETES_INGRESSES_NAMES \ 18 | + KUBERNETES_DEPLOYMENTS_NAMES\ 19 | + KUBERNETES_PODS_NAMES \ 20 | + KUBERNETES_REPLICASETS_NAMES \ 21 | + KUBERNETES_SECRETS_NAMES \ 22 | + KUBERNETES_SERVICES_NAMES \ 23 | + KUBERNETES_STATEFULSETS_NAMES 24 | 25 | def is_object_allowed(k8s_object): 26 | return k8s_object in ALLOWED_K8S_OBJECTS 27 | 28 | def get_watcher(k8s_object): 29 | ''' 30 | get_watcher(k8s_object) will return the desired watcher 31 | based on the k8s_object (name) given as argument. 32 | ''' 33 | if k8s_object in KUBERNETES_CONFIGMAPS_NAMES: 34 | return get_configmap_watcher() 35 | if k8s_object in KUBERNETES_DAEMONSETS_NAMES: 36 | return get_daemonset_watcher() 37 | if k8s_object in KUBERNETES_DEPLOYMENTS_NAMES: 38 | return get_deployment_watcher() 39 | if k8s_object in KUBERNETES_INGRESSES_NAMES: 40 | return get_ingress_watcher() 41 | if k8s_object in KUBERNETES_PODS_NAMES: 42 | return get_pod_watcher() 43 | if k8s_object in KUBERNETES_REPLICASETS_NAMES: 44 | return get_replicaset_watcher() 45 | if k8s_object in KUBERNETES_SECRETS_NAMES: 46 | return get_secret_watcher() 47 | if k8s_object in KUBERNETES_SERVICES_NAMES: 48 | return get_service_watcher() 49 | if k8s_object in KUBERNETES_STATEFULSETS_NAMES: 50 | return get_statefulset_watcher() 51 | -------------------------------------------------------------------------------- /src/k8s/core/v1.py: -------------------------------------------------------------------------------- 1 | from kubernetes import client 2 | from k8s import Watcher 3 | 4 | _pod_watcher = None 5 | _service_watcher = None 6 | _secret_watcher = None 7 | _configmap_watcher = None 8 | 9 | def get_configmap_watcher(namespace=None): 10 | global _configmap_watcher 11 | if _configmap_watcher is None: 12 | _configmap_watcher = ConfigmapWatcher(namespace=namespace) 13 | return _configmap_watcher 14 | 15 | def get_pod_watcher(namespace=None): 16 | global _pod_watcher 17 | if _pod_watcher is None: 18 | _pod_watcher = PodWatcher(namespace=namespace) 19 | return _pod_watcher 20 | 21 | def get_service_watcher(namespace=None): 22 | global _service_watcher 23 | if _service_watcher is None: 24 | _service_watcher = ServiceWatcher(namespace=namespace) 25 | return _service_watcher 26 | 27 | def get_secret_watcher(namespace=None): 28 | global _secret_watcher 29 | if _secret_watcher is None: 30 | _secret_watcher = SecretWatcher(namespace=namespace) 31 | return _secret_watcher 32 | 33 | class ConfigmapWatcher(Watcher): 34 | def __init__(self, namespace=None, auto_start=False): 35 | self.namespace=namespace 36 | super().__init__(auto_start) 37 | 38 | def get_stream_args(self): 39 | if self.namespace is None: 40 | return [client.CoreV1Api().list_config_map_for_all_namespaces] 41 | 42 | return [client.CoreV1Api().list_namespaced_config_map, self.namespace] 43 | 44 | def get_stream_kwargs(self): 45 | return self._get_stream_default_kwargs() 46 | 47 | class PodWatcher(Watcher): 48 | def __init__(self, namespace=None, auto_start=False): 49 | self.namespace=namespace 50 | super().__init__(auto_start) 51 | 52 | def get_stream_args(self): 53 | if self.namespace is None: 54 | return [client.CoreV1Api().list_pod_for_all_namespaces] 55 | 56 | return [client.CoreV1Api().list_namespaced_pod, self.namespace] 57 | 58 | def get_stream_kwargs(self): 59 | return self._get_stream_default_kwargs() 60 | 61 | class SecretWatcher(Watcher): 62 | def __init__(self, namespace=None, auto_start=False): 63 | self.namespace=namespace 64 | super().__init__(auto_start) 65 | 66 | def get_stream_args(self): 67 | if self.namespace is None: 68 | return [client.CoreV1Api().list_secret_for_all_namespaces] 69 | 70 | return [client.CoreV1Api().list_namespaced_secret, self.namespace] 71 | 72 | def get_stream_kwargs(self): 73 | return self._get_stream_default_kwargs() 74 | 75 | class ServiceWatcher(Watcher): 76 | def __init__(self, namespace=None, auto_start=False): 77 | self.namespace=namespace 78 | super().__init__(auto_start) 79 | 80 | def get_stream_args(self): 81 | if self.namespace is None: 82 | return [client.CoreV1Api().list_service_for_all_namespaces] 83 | 84 | return [client.CoreV1Api().list_namespaced_service, self.namespace] 85 | 86 | def get_stream_kwargs(self): 87 | return self._get_stream_default_kwargs() 88 | -------------------------------------------------------------------------------- /tests/k8s/core/test_v1.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from kubernetes import client 3 | from k8s.core.v1 import get_configmap_watcher, get_pod_watcher,\ 4 | get_service_watcher, get_secret_watcher,\ 5 | ConfigmapWatcher, PodWatcher, SecretWatcher, ServiceWatcher 6 | from tests import TestCaseWatcher 7 | 8 | 9 | class ConfigmapWatcherTest(TestCaseWatcher): 10 | def test_args_without_namespace(self): 11 | actual = ConfigmapWatcher().get_stream_args() 12 | expected = [client.CoreV1Api().list_config_map_for_all_namespaces] 13 | self.check_args(actual, expected) 14 | 15 | 16 | def test_args_with_namespace(self): 17 | namespace = "test" 18 | actual = ConfigmapWatcher(namespace).get_stream_args() 19 | expected = [ 20 | client.CoreV1Api().list_namespaced_config_map, 21 | namespace 22 | ] 23 | self.check_args(actual, expected) 24 | 25 | def test_get_configmap_watcher(self): 26 | watcher = get_configmap_watcher() 27 | self.assertTrue(watcher) 28 | 29 | 30 | class PodWatcherTest(TestCaseWatcher): 31 | def test_args_without_namespace(self): 32 | actual = PodWatcher().get_stream_args() 33 | expected = [client.CoreV1Api().list_pod_for_all_namespaces] 34 | self.check_args(actual, expected) 35 | 36 | 37 | def test_args_with_namespace(self): 38 | namespace = "test" 39 | actual = PodWatcher(namespace).get_stream_args() 40 | expected = [ 41 | client.CoreV1Api().list_namespaced_pod, 42 | namespace 43 | ] 44 | self.check_args(actual, expected) 45 | 46 | def test_get_pod_watcher(self): 47 | watcher = get_pod_watcher() 48 | self.assertTrue(watcher) 49 | 50 | 51 | class SecretWatcherTest(TestCaseWatcher): 52 | def test_args_without_namespace(self): 53 | actual = SecretWatcher().get_stream_args() 54 | expected = [client.CoreV1Api().list_secret_for_all_namespaces] 55 | self.check_args(actual, expected) 56 | 57 | 58 | def test_args_with_namespace(self): 59 | namespace = "test" 60 | actual = SecretWatcher(namespace).get_stream_args() 61 | expected = [ 62 | client.CoreV1Api().list_namespaced_secret, 63 | namespace 64 | ] 65 | self.check_args(actual, expected) 66 | 67 | def test_get_secret_watcher(self): 68 | watcher = get_secret_watcher() 69 | self.assertTrue(watcher) 70 | 71 | 72 | class ServiceWatcherTest(TestCaseWatcher): 73 | def test_args_without_namespace(self): 74 | actual = ServiceWatcher().get_stream_args() 75 | expected = [client.CoreV1Api().list_service_for_all_namespaces] 76 | self.check_args(actual, expected) 77 | 78 | 79 | def test_args_with_namespace(self): 80 | namespace = "test" 81 | actual = ServiceWatcher(namespace).get_stream_args() 82 | expected = [ 83 | client.CoreV1Api().list_namespaced_service, 84 | namespace 85 | ] 86 | self.check_args(actual, expected) 87 | 88 | def test_get_service_watcher(self): 89 | watcher = get_service_watcher() 90 | self.assertTrue(watcher) 91 | 92 | 93 | if __name__ == '__main__': 94 | unittest.main() -------------------------------------------------------------------------------- /src/k8s/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | from http import HTTPStatus 4 | from ssl import SSLError 5 | from urllib3.exceptions import MaxRetryError 6 | from threading import Thread 7 | import kubernetes 8 | from config.logger import get_logger 9 | 10 | _DEFAULT_TIMEOUT=60 11 | _DEFAULT_WAITING_TIME_AFTER_NET_ERROR=3 12 | _INITIAL_RESOURCE_VERSION=None 13 | _REGEX_NEWER_RESOURCE_VERSION = '^.+\((\d+)\)$' 14 | _REGEX_NEWER_RESOURCE_VERSION_POSITION = 1 15 | 16 | logger = get_logger() 17 | 18 | def load_kubeconfig(): 19 | kubernetes.config.load_kube_config() 20 | 21 | class Watcher(Thread): 22 | def __init__(self, auto_start=False): 23 | super().__init__() 24 | self.observers = list() 25 | self.resource_version = _INITIAL_RESOURCE_VERSION 26 | self.waiting_time_after_net_error = _DEFAULT_WAITING_TIME_AFTER_NET_ERROR 27 | self.timeout = _DEFAULT_TIMEOUT 28 | if auto_start: 29 | self.start() 30 | 31 | def _get_stream_default_kwargs(self): 32 | return dict( 33 | resource_version=self.resource_version, 34 | _request_timeout=self.timeout 35 | ) 36 | 37 | def _notify(self, event): 38 | for o in self.observers: 39 | o.notify(event) 40 | 41 | def register(self, observer): 42 | self.observers.append(observer) 43 | 44 | def run(self): 45 | while True: 46 | try: 47 | self._stream() 48 | except SSLError as e: 49 | if e.args and 'timed out' in e.args: 50 | logger.debug(f'Timed out watcher loop: {e}') 51 | else: 52 | logger.debug(f'SSLError watcher loop: {e}') 53 | except kubernetes.client.rest.ApiException as e: 54 | logger.debug(f'Watcher loop ApiException: {e}') 55 | if e.status == HTTPStatus.GONE: 56 | newer_resource_version = Watcher._get_resource_version_from_exception(e.reason) 57 | self._update_resource_version(new_resource_version=newer_resource_version) 58 | except MaxRetryError as e: 59 | logger.debug(f"watcher loop MaxRetryError {e}") 60 | time.sleep(_DEFAULT_WAITING_TIME_AFTER_NET_ERROR) 61 | except Exception as e: 62 | logger.debug(f"watcher loop Exception {e}") 63 | 64 | @staticmethod 65 | def _get_resource_version_from_exception(reason): 66 | match = re.match(_REGEX_NEWER_RESOURCE_VERSION, reason) 67 | if match and match.group(_REGEX_NEWER_RESOURCE_VERSION_POSITION): 68 | return int(match.group(_REGEX_NEWER_RESOURCE_VERSION_POSITION)) 69 | return None 70 | 71 | def _stream(self): 72 | args = self.get_stream_args() 73 | kwargs = self.get_stream_kwargs() 74 | for event in kubernetes.watch.Watch().stream(*args, **kwargs): 75 | self._update_resource_version(obj=event) 76 | self._notify(event) 77 | 78 | def _update_resource_version(self, obj=None, new_resource_version=None): 79 | if new_resource_version: 80 | self.resource_version = new_resource_version 81 | return 82 | 83 | if obj: 84 | k8s_object = obj['object'] if 'object' in obj else obj 85 | self.resource_version = int(k8s_object.metadata.resource_version) 86 | -------------------------------------------------------------------------------- /tests/k8s/apps/test_v1.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from kubernetes import client 3 | from k8s.apps.v1 import get_daemonset_watcher, get_deployment_watcher,\ 4 | get_replicaset_watcher, get_statefulset_watcher,\ 5 | DaemonsetWatcher, DeploymentWatcher, ReplicasetWatcher, StatefulsetWatcher 6 | from tests import TestCaseWatcher 7 | 8 | 9 | class DaemonsetWatcherTest(TestCaseWatcher): 10 | def test_args_without_namespace(self): 11 | actual = DaemonsetWatcher().get_stream_args() 12 | expected = [client.AppsV1Api().list_daemon_set_for_all_namespaces] 13 | self.check_args(actual, expected) 14 | 15 | 16 | def test_args_with_namespace(self): 17 | namespace = "test" 18 | actual = DeploymentWatcher(namespace).get_stream_args() 19 | expected = [ 20 | client.AppsV1Api().list_namespaced_daemon_set, 21 | namespace 22 | ] 23 | self.check_args(actual, expected) 24 | 25 | def test_get_daemonset_watcher(self): 26 | watcher = get_daemonset_watcher() 27 | self.assertTrue(watcher) 28 | 29 | 30 | class DeploymentWatcherTest(TestCaseWatcher): 31 | def test_args_without_namespace(self): 32 | actual = DeploymentWatcher().get_stream_args() 33 | expected = [client.AppsV1Api().list_deployment_for_all_namespaces] 34 | self.check_args(actual, expected) 35 | 36 | 37 | def test_args_with_namespace(self): 38 | namespace = "test" 39 | actual = DeploymentWatcher(namespace).get_stream_args() 40 | expected = [ 41 | client.AppsV1Api().list_namespaced_deployment, 42 | namespace 43 | ] 44 | self.check_args(actual, expected) 45 | 46 | def test_get_deployment_watcher(self): 47 | watcher = get_deployment_watcher() 48 | self.assertTrue(watcher) 49 | 50 | 51 | class ReplicasetWatcherTest(TestCaseWatcher): 52 | def test_args_without_namespace(self): 53 | actual = ReplicasetWatcher().get_stream_args() 54 | expected = [client.AppsV1Api().list_replica_set_for_all_namespaces] 55 | self.check_args(actual, expected) 56 | 57 | 58 | def test_args_with_namespace(self): 59 | namespace = "test" 60 | actual = ReplicasetWatcher(namespace).get_stream_args() 61 | expected = [ 62 | client.AppsV1Api().list_namespaced_replica_set, 63 | namespace 64 | ] 65 | self.check_args(actual, expected) 66 | 67 | def test_get_replicaset_watcher(self): 68 | watcher = get_replicaset_watcher() 69 | self.assertTrue(watcher) 70 | 71 | 72 | class StatefulsetWatcherTest(TestCaseWatcher): 73 | def test_args_without_namespace(self): 74 | actual = StatefulsetWatcher().get_stream_args() 75 | expected = [client.AppsV1Api().list_stateful_set_for_all_namespaces] 76 | self.check_args(actual, expected) 77 | 78 | 79 | def test_args_with_namespace(self): 80 | namespace = "test" 81 | actual = StatefulsetWatcher(namespace).get_stream_args() 82 | expected = [ 83 | client.AppsV1Api().list_namespaced_stateful_set, 84 | namespace 85 | ] 86 | self.check_args(actual, expected) 87 | 88 | def test_get_statefulset_watcher(self): 89 | watcher = get_statefulset_watcher() 90 | self.assertTrue(watcher) 91 | 92 | 93 | if __name__ == '__main__': 94 | unittest.main() -------------------------------------------------------------------------------- /src/k8s/apps/v1.py: -------------------------------------------------------------------------------- 1 | from kubernetes import client, watch 2 | from k8s import Watcher 3 | 4 | _daemonset_watcher = None 5 | _deployment_watcher = None 6 | _replicaset_watcher = None 7 | _statefulset_watcher = None 8 | 9 | def get_deployment_watcher(namespace=None): 10 | global _deployment_watcher 11 | if _deployment_watcher is None: 12 | _deployment_watcher = DeploymentWatcher(namespace=namespace) 13 | return _deployment_watcher 14 | 15 | def get_daemonset_watcher(namespace=None): 16 | global _daemonset_watcher 17 | if _daemonset_watcher is None: 18 | _daemonset_watcher = DaemonsetWatcher(namespace=namespace) 19 | return _daemonset_watcher 20 | 21 | def get_replicaset_watcher(namespace=None): 22 | global _replicaset_watcher 23 | if _replicaset_watcher is None: 24 | _replicaset_watcher = ReplicasetWatcher(namespace=namespace) 25 | return _replicaset_watcher 26 | 27 | def get_statefulset_watcher(namespace=None): 28 | global _statefulset_watcher 29 | if _statefulset_watcher is None: 30 | _statefulset_watcher = StatefulsetWatcher(namespace=namespace) 31 | return _statefulset_watcher 32 | 33 | class DaemonsetWatcher(Watcher): 34 | def __init__(self, namespace=None, auto_start=False): 35 | self.namespace=namespace 36 | super().__init__(auto_start) 37 | 38 | def get_stream_args(self): 39 | if self.namespace is None: 40 | return [client.AppsV1Api().list_daemon_set_for_all_namespaces] 41 | 42 | return [client.AppsV1Api().list_namespaced_daemon_set,\ 43 | self.namespace] 44 | 45 | def get_stream_kwargs(self): 46 | return self._get_stream_default_kwargs() 47 | 48 | class DeploymentWatcher(Watcher): 49 | def __init__(self, namespace=None, auto_start=False): 50 | self.namespace=namespace 51 | super().__init__(auto_start) 52 | 53 | def get_stream_args(self): 54 | if self.namespace is None: 55 | return [client.AppsV1Api().list_deployment_for_all_namespaces] 56 | 57 | return [client.AppsV1Api().list_namespaced_deployment,\ 58 | self.namespace] 59 | 60 | def get_stream_kwargs(self): 61 | return self._get_stream_default_kwargs() 62 | 63 | class ReplicasetWatcher(Watcher): 64 | def __init__(self, namespace=None, auto_start=False): 65 | self.namespace=namespace 66 | super().__init__(auto_start) 67 | 68 | def get_stream_args(self): 69 | if self.namespace is None: 70 | return [client.AppsV1Api().list_replica_set_for_all_namespaces] 71 | 72 | return [client.AppsV1Api().list_namespaced_replica_set,\ 73 | self.namespace] 74 | 75 | def get_stream_kwargs(self): 76 | return self._get_stream_default_kwargs() 77 | 78 | class StatefulsetWatcher(Watcher): 79 | def __init__(self, namespace=None, auto_start=False): 80 | self.namespace=namespace 81 | super().__init__(auto_start) 82 | 83 | def get_stream_args(self): 84 | if self.namespace is None: 85 | return [client.AppsV1Api().list_stateful_set_for_all_namespaces] 86 | 87 | return [client.AppsV1Api().list_namespaced_replica_set,\ 88 | self.namespace] 89 | 90 | def get_stream_kwargs(self): 91 | return self._get_stream_default_kwargs() 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Salmorejo 2 | 3 | Salmorejo is a command line tool which will help you to run your own Python scripts in order to test, debug and/or analyze the status of your Kubernetes cluster in real time. 4 | 5 | > Salmorejo is under heavy development 6 | 7 | ## How does it work? 8 | 9 | Salmorejo will connect to your "current-connected" Kubernetes cluster (check `$ kubectl cluster-info`) and it will received changes in the desired objects. These events will be forwarded to your custom Python script. 10 | 11 | Your python script must contain a function call `callback` which has the argument `event`. Every event will contain 3 main fields: 12 | 13 | * 'type': The type of event such as "ADDED", "DELETED", etc. 14 | * 'raw_object': a dict representing the watched object. 15 | * 'object': A model representation of raw_object. The name of 16 | model will be determined based on 17 | the func's doc string. If it cannot be determined, 18 | 'object' value will be the same as 'raw_object'. 19 | 20 | > Event information has been defined by the [kubernetes library](https://github.com/kubernetes-client/python). 21 | 22 | ## Supported Objects 23 | 24 | Currently, Salmorejo supports: 25 | 26 | * Configmaps ("configmap", "configmaps", "cm") 27 | * Daemonsets ("daemonset", "daemonsets", "ds") 28 | * Deployments ("deployments", "deployment") 29 | * Ingresses ("ingress", "ingresses", "ing") 30 | * Pods ("pods", "pod", "po") 31 | * Secrets ("secret", "secrets") 32 | * Services ("services", "service", "svc") 33 | * Statefulsets ("statefulset", "statefulsets", "sts") 34 | 35 | ## Installation 36 | 37 | ### From Pypi 38 | 39 | ```bash 40 | $ python -m pip install salmorejo 41 | ``` 42 | 43 | ### From Code 44 | 45 | ```bash 46 | $ python -m pip install -e . 47 | ``` 48 | 49 | ## How to use it? 50 | 51 | ### CLI 52 | 53 | ```bash 54 | $ salmorejo watch 55 | ``` 56 | 57 | ### From Code repository 58 | 59 | ```bash 60 | $ python main.py watch 61 | ``` 62 | 63 | ### Example 64 | 65 | ```bash 66 | $ salmorejo watch ./scripts/counter.py pod,svc,deployments 67 | 68 | +------------+--------------------+-------+ 69 | | KIND | NAMESPACE | COUNT | 70 | +------------+--------------------+-------+ 71 | | Service | default | 2 | 72 | +------------+--------------------+-------+ 73 | | Deployment | kube-system | 1 | 74 | +------------+--------------------+-------+ 75 | | Pod | kube-system | 8 | 76 | +------------+--------------------+-------+ 77 | | Deployment | local-path-storage | 1 | 78 | +------------+--------------------+-------+ 79 | | Service | kube-system | 1 | 80 | +------------+--------------------+-------+ 81 | | Pod | local-path-storage | 1 | 82 | +------------+--------------------+-------+ 83 | | Pod | default | 4 | 84 | +------------+--------------------+-------+ 85 | ``` 86 | 87 | ### Examples 88 | 89 | Examples of Scripts can be found under [here](./scripts/) 90 | 91 | 92 | ## Why Python 93 | 94 | Although [Go](https://go.dev/) is the lingua franca to code against Kubernetes, [Python](https://go.dev/) could be considered the most used programming language by SysAdmins, SREs or Platform Engineers. Salmorejo was thought for this kind of people, and we hope that they enjoy it. 95 | 96 | ## What actually is Salmorejo? 97 | 98 | Salmorejo is a traditional Andalusian food, originally from Córdoba. [Wiki](https://en.wikipedia.org/wiki/Salmorejo). 99 | -------------------------------------------------------------------------------- /scripts/dashboard.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from queue import deque 3 | 4 | import dash 5 | from dash import dcc, html 6 | from dash.dependencies import Output, Input 7 | import dash_bootstrap_components as dbc 8 | import plotly 9 | import plotly.graph_objs as go 10 | 11 | MAX = 13 12 | MAX_PODS = 20 13 | 14 | X = deque(maxlen = MAX) 15 | X.append(1) 16 | 17 | PODS = deque(maxlen = MAX) 18 | PODS.append(0) 19 | 20 | LOG = deque(maxlen = MAX) 21 | 22 | counters = dict() 23 | 24 | 25 | def callback(event): 26 | global counters 27 | 28 | if event['raw_object']['metadata']['namespace'] != "default": 29 | return 30 | 31 | name = event['raw_object']['metadata']['name'] 32 | kind = event['raw_object']['kind'] 33 | t = event['type'] 34 | 35 | current_count = counters.get(kind, 0) 36 | if t == 'ADDED': 37 | counters[kind] = current_count + 1 38 | 39 | if t == 'DELETED': 40 | counters[kind] = max(current_count - 1, 0) 41 | 42 | LOG.append(f"{kind} {name} has been {t}") 43 | 44 | 45 | # layouts 46 | 47 | def log_layout(): 48 | return html.Div( 49 | className="border", 50 | style={ 51 | 'padding': '50px', 52 | 'height': '600px', 53 | }, 54 | children=[ 55 | html.H2(children='Log', style={"text-align": "center"}), 56 | html.Div( 57 | id='live-log', 58 | className="border", 59 | style={ 60 | 'whiteSpace': 'pre-line', 61 | 'text-align': 'left', 62 | 'background-color': bg_graph_color, 63 | 'color': text_color, 64 | 'padding': '50px', 65 | 'font-size': '20px', 66 | 'height': '450px', 67 | } 68 | ), 69 | dcc.Interval( 70 | id = 'log-update', 71 | interval = 1000, 72 | n_intervals = 0 73 | ) 74 | ] 75 | ) 76 | 77 | def gauge_layout(): 78 | return html.Div( 79 | className="border", 80 | style={ 81 | 'padding': '50px', 82 | 'height': '600px', 83 | }, 84 | children=[ 85 | html.H2(children='State', style={"text-align": "center"}), 86 | dcc.Graph(id = 'live-gauge', animate = True), 87 | dcc.Interval( 88 | id = 'gauge-update', 89 | interval = 1000, 90 | n_intervals = 0 91 | ) 92 | ] 93 | ) 94 | 95 | def scatter_layout(): 96 | return html.Div( 97 | className="border", 98 | style={ 99 | 'padding': '50px', 100 | }, 101 | children=[ 102 | html.H2(children='History', style={"text-align": "center"}), 103 | dcc.Graph(id = 'live-graph', animate = True), 104 | dcc.Interval( 105 | id = 'graph-update', 106 | interval = 1000, 107 | n_intervals = 0 108 | ) 109 | ] 110 | ) 111 | 112 | app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) 113 | 114 | bg_color = '#262A2F' 115 | bg_graph_color = '#32383E' 116 | text_color = 'white' 117 | line_color = 'red' 118 | 119 | app.layout = dbc.Container( 120 | [ 121 | dbc.Row( 122 | dbc.Col( 123 | html.H1( 124 | children='Pods Dashboard', 125 | style={ 126 | "text-align": "center", 127 | 'padding': '50px', 128 | } 129 | ) 130 | ) 131 | ), 132 | dbc.Row( 133 | dbc.Col( 134 | scatter_layout() 135 | ) 136 | ), 137 | dbc.Row( 138 | [ 139 | dbc.Col(gauge_layout()), 140 | dbc.Col(log_layout()) 141 | ] 142 | ), 143 | ], 144 | fluid=True, 145 | style={ 146 | 'background-color': bg_color, 147 | 'color': text_color, 148 | 'padding': '10px', 149 | } 150 | ) 151 | 152 | 153 | # callbacks 154 | 155 | @app.callback( 156 | Output('live-graph', 'figure'), 157 | [ Input('graph-update', 'n_intervals') ] 158 | ) 159 | def update_graph(n): 160 | global counters 161 | 162 | X.append(X[-1]+1) 163 | PODS.append(counters.get("Pod", 0)) 164 | 165 | pods = plotly.graph_objs.Scatter( 166 | x=list(X), 167 | y=list(PODS), 168 | name='Pods', 169 | mode='lines+markers' 170 | ) 171 | 172 | 173 | return { 174 | 'data': [pods], 175 | 'layout' : go.Layout( 176 | xaxis=dict(range=[min(X),max(X)]),yaxis = dict(range = [0,max(PODS) + 2]), 177 | )} 178 | 179 | 180 | @app.callback( 181 | Output('live-gauge', 'figure'), 182 | [ Input('gauge-update', 'n_intervals') ] 183 | ) 184 | def update_gauge(n): 185 | gauge = plotly.graph_objs.Indicator( 186 | value=PODS[-1], 187 | name='Pods', 188 | mode='gauge+number', 189 | gauge={ 190 | 'bar': {'color': text_color}, 191 | 'bordercolor': text_color, 192 | 'bgcolor': bg_graph_color, 193 | 'axis': {'range': [0, MAX_PODS], 'tickcolor': '#39405F'}, 194 | } 195 | ) 196 | 197 | return { 198 | 'data': [gauge], 199 | 'layout' : go.Layout( 200 | xaxis=dict(range=[0, MAX_PODS]), 201 | ) 202 | } 203 | 204 | 205 | @app.callback( 206 | Output('live-log', 'children'), 207 | [ Input('log-update', 'n_intervals') ] 208 | ) 209 | def update_log(n): 210 | return "\n".join([str(l) for l in list(LOG)]) 211 | 212 | 213 | # Web server 214 | threading.Thread(target=app.run_server).start() 215 | --------------------------------------------------------------------------------