id: k8s_namespace_health_digest
namespace: system
description: |
Clusterify runs once a week and does three things: lists all pods
across every namespace, builds a health digest (pods restarting often,
health per namespace, top CPU and memory requesters), then posts the
report to Slack.
inputs:
- id: slack_webhook
type: STRING
description: Incoming webhook URL of the Slack channel for the weekly digest
tasks:
- id: list_pods
type: io.kestra.plugin.scripts.shell.Commands
description: lists all the pods in every namespace
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
user: "0"
containerImage: bitnami/kubectl
inputFiles:
client.crt: "{{ secret('CLIENT_CRT') }}"
client.key: "{{ secret('CLIENT_KEY') }}"
outputFiles:
- pods.json
commands:
- kubectl get pods -A -o json --client-certificate=client.crt
--client-key=client.key --server={{ secret('MASTERURL') }}
--insecure-skip-tls-verify=true > pods.json
- id: build_digest
type: io.kestra.plugin.scripts.python.Script
description: reads the pods JSON, pulls restart counts and resource requests out
of each pod, and builds the digest text
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
user: "0"
containerImage: python:3.11
dependencies:
- kestra
script: |
import json
from datetime import datetime, timezone
from kestra import Kestra
with open('{{ outputs.list_pods.outputFiles['pods.json'] }}', 'r') as f:
pods = json.load(f)['items']
def parse_cpu(s):
s = str(s or "0").strip()
return float(s[:-1]) / 1000 if s.endswith("m") else float(s)
def parse_mem(s):
s = str(s or "0").strip()
units = {"Ki": 1024, "Mi": 1024**2, "Gi": 1024**3,
"K": 1000, "M": 1000**2, "G": 1000**3}
for u, mult in units.items():
if s.endswith(u):
return float(s[:-len(u)]) * mult
return float(s)
# pending pods have no containerStatuses yet; containers may omit resources
rows = []
for pod in pods:
meta = pod.get("metadata", {})
spec = pod.get("spec", {})
status = pod.get("status", {})
rows.append({
"ns": meta.get("namespace", "?"),
"name": meta.get("name", "?"),
"phase": status.get("phase", "?"),
"restarts": sum(c.get("restartCount", 0)
for c in (status.get("containerStatuses") or [])),
"cpu": sum(parse_cpu((c.get("resources") or {}).get("requests", {}).get("cpu"))
for c in spec.get("containers", [])),
"mem": sum(parse_mem((c.get("resources") or {}).get("requests", {}).get("memory"))
for c in spec.get("containers", [])),
})
ns_health = {}
for r in rows:
h = ns_health.setdefault(r["ns"], {"pods": 0, "running": 0, "restarts": 0})
h["pods"] += 1
if r["phase"] == "Running":
h["running"] += 1
h["restarts"] += r["restarts"]
top_restarts = sorted(rows, key=lambda r: r["restarts"], reverse=True)[:5]
top_cpu = sorted(rows, key=lambda r: r["cpu"], reverse=True)[:5]
top_mem = sorted(rows, key=lambda r: r["mem"], reverse=True)[:5]
def fmt_cpu(v):
return f"{v * 1000:.0f}m" if v < 1 else f"{v:.2f}"
def fmt_mem(v):
for unit, mult in (("Gi", 1024**3), ("Mi", 1024**2), ("Ki", 1024)):
if v >= mult:
return f"{v / mult:.0f}{unit}"
return f"{v:.0f}B"
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
lines = [f"*Clusterify - weekly K8s digest ({today})*", ""]
for ns in sorted(ns_health):
h = ns_health[ns]
lines.append(f"- {ns}: {h['pods']} pods, {h['running']} running, {h['restarts']} restarts")
lines.append("")
lines.append("*Top restarts*")
lines.extend(f"- {r['name']} ({r['ns']}): {r['restarts']}" for r in top_restarts)
lines.append("")
lines.append("*Top CPU requesters*")
lines.extend(f"- {r['name']} ({r['ns']}): {fmt_cpu(r['cpu'])}" for r in top_cpu)
lines.append("")
lines.append("*Top memory requesters*")
lines.extend(f"- {r['name']} ({r['ns']}): {fmt_mem(r['mem'])}" for r in top_mem)
digest = "\n".join(lines)
Kestra.outputs({"digest": digest})
- id: notify_slack
type: io.kestra.plugin.notifications.slack.SlackIncomingWebhook
description: sends the digest to Slack
url: "{{ inputs.slack_webhook }}"
messageText: "{{ outputs.build_digest.vars.digest }}"
triggers:
- id: weekly
type: io.kestra.plugin.core.trigger.Schedule
cron: "0 9 * * 1"
timezone: UTC
inputs:
slack_webhook: "{{ secret('SLACK_WEBHOOK') }}"