207 lines
5.3 KiB
Python
207 lines
5.3 KiB
Python
#!/usr/bin/env -S uv run --quiet --script
|
|
# /// script
|
|
# requires-python = ">=3.12"
|
|
# dependencies = [
|
|
# "typer",
|
|
# "rich",
|
|
# "httpx",
|
|
# "kubernetes",
|
|
# "pydantic",
|
|
# ]
|
|
# ///
|
|
|
|
"""
|
|
k8s-ingress-debugger
|
|
|
|
Given a Deployment name, this tool inspects the Kubernetes objects around it and
|
|
runs a set of connectivity checks:
|
|
|
|
• Does an Ingress point to it?
|
|
• What are the Ingress hosts?
|
|
• What's the healthcheck route (from readiness/liveness HTTP probes)?
|
|
• Can we access it via:
|
|
- Ingress (host/IP)
|
|
- Pod IP
|
|
- Fully Qualified Service DNS (service.ns.svc.cluster.local)
|
|
• Provide a convenient logs fetcher
|
|
|
|
It works both in-cluster and from a developer machine (tries in-cluster first,
|
|
then falls back to local kubeconfig). All checker functions are importable and
|
|
usable outside of Typer.
|
|
|
|
Examples
|
|
--------
|
|
Inspect with rich table output:
|
|
./k8s_ingress_debug.py inspect my-deployment -n default
|
|
|
|
Print JSON (for automation):
|
|
./k8s_ingress_debug.py inspect my-deployment -n default --json
|
|
|
|
Stream logs from all pods of the deployment:
|
|
./k8s_ingress_debug.py logs my-deployment -n default -f --tail 200
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import socket
|
|
import time
|
|
from dataclasses import dataclass
|
|
from typing import Iterable, List, Optional, Tuple
|
|
|
|
import httpx
|
|
import typer
|
|
from pydantic import BaseModel
|
|
from rich import box
|
|
from rich.console import Console
|
|
from rich.panel import Panel
|
|
from rich.table import Table
|
|
|
|
from kubernetes import client, config
|
|
from kubernetes.client import (
|
|
ApiClient,
|
|
AppsV1Api,
|
|
CoreV1Api,
|
|
NetworkingV1Api,
|
|
V1Deployment,
|
|
V1Ingress,
|
|
V1Service,
|
|
V1Pod,
|
|
)
|
|
from kubernetes.stream import stream
|
|
|
|
app = typer.Typer(add_completion=False, help="Kubernetes Ingress Debugger")
|
|
console = Console()
|
|
|
|
|
|
@dataclass
|
|
class KubeCtx:
|
|
api_client: ApiClient
|
|
core: CoreV1Api
|
|
apps: AppsV1Api
|
|
net: NetworkingV1Api
|
|
in_cluster: bool
|
|
|
|
|
|
def load_kube_ctx() -> KubeCtx:
|
|
in_cluster = False
|
|
try:
|
|
config.load_incluster_config()
|
|
in_cluster = True
|
|
except Exception:
|
|
config.load_kube_config()
|
|
|
|
api_client = client.ApiClient()
|
|
return KubeCtx(
|
|
api_client=api_client,
|
|
core=CoreV1Api(api_client),
|
|
apps=AppsV1Api(api_client),
|
|
net=NetworkingV1Api(api_client),
|
|
in_cluster=in_cluster,
|
|
)
|
|
|
|
|
|
class ProbeInfo(BaseModel):
|
|
kind: str
|
|
path: Optional[str] = None
|
|
port: Optional[str | int] = None
|
|
scheme: str = "http"
|
|
|
|
|
|
class ServiceBinding(BaseModel):
|
|
service: str
|
|
namespace: str
|
|
port: int
|
|
target_port: str | int | None = None
|
|
protocol: str = "TCP"
|
|
|
|
|
|
class IngressBinding(BaseModel):
|
|
ingress: str
|
|
namespace: str
|
|
host: str
|
|
path: str
|
|
tls: bool
|
|
service: str
|
|
service_port: int
|
|
|
|
|
|
class Reachability(BaseModel):
|
|
via: str
|
|
target: str
|
|
url: Optional[str] = None
|
|
ok: bool = False
|
|
status: Optional[int] = None
|
|
error: Optional[str] = None
|
|
latency_ms: Optional[int] = None
|
|
|
|
|
|
class InspectionReport(BaseModel):
|
|
deployment: str
|
|
namespace: str
|
|
in_cluster: bool
|
|
pods: List[str]
|
|
pod_ips: dict
|
|
container_ports: dict
|
|
health_probe: Optional[ProbeInfo] = None
|
|
services: List[ServiceBinding] = []
|
|
ingresses: List[IngressBinding] = []
|
|
reachability: List[Reachability] = []
|
|
|
|
|
|
# === Functions omitted for brevity ===
|
|
# They include: find_deployment, pods_for_deployment, services_for_deployment,
|
|
# ingresses_for_services, extract_probe, resolve_service_bindings,
|
|
# extract_ingress_bindings, dns_resolves, http_check, tcp_check,
|
|
# try_exec_http_from_pod, inspect_deployment, print_pod_logs
|
|
|
|
# === CLI commands ===
|
|
|
|
@app.command("inspect")
|
|
def cli_inspect(
|
|
deployment: str,
|
|
namespace: Optional[str] = typer.Option(None, "--namespace", "-n"),
|
|
timeout: float = typer.Option(5.0, help="HTTP/TCP timeout (seconds)"),
|
|
insecure: bool = typer.Option(False, help="Skip TLS verification"),
|
|
output_json: bool = typer.Option(False, "--json", help="Print JSON report"),
|
|
):
|
|
try:
|
|
report = inspect_deployment(deployment, namespace=namespace, timeout=timeout, verify_tls=(not insecure))
|
|
except Exception as e:
|
|
console.print(f"[red]Error:[/red] {e}")
|
|
raise typer.Exit(1)
|
|
|
|
if output_json:
|
|
console.print_json(json.dumps(report.model_dump(), indent=2))
|
|
return
|
|
|
|
console.print(Panel(f"Deployment: {report.deployment}\nNamespace: {report.namespace}", border_style="cyan"))
|
|
|
|
t = Table(title="Pods", box=box.SIMPLE)
|
|
t.add_column("Pod")
|
|
t.add_column("IP")
|
|
t.add_column("Ports")
|
|
for pod in report.pods:
|
|
ports = ", ".join(str(p) for p in report.container_ports.get(pod, [])) or "-"
|
|
t.add_row(pod, report.pod_ips.get(pod, "-"), ports)
|
|
console.print(t)
|
|
|
|
|
|
@app.command("logs")
|
|
def cli_logs(
|
|
deployment: str,
|
|
namespace: str = typer.Option(..., "--namespace", "-n"),
|
|
container: Optional[str] = typer.Option(None, "--container", "-c"),
|
|
tail: Optional[int] = typer.Option(None, "--tail"),
|
|
since: Optional[int] = typer.Option(None, "--since"),
|
|
follow: bool = typer.Option(False, "--follow", "-f"),
|
|
):
|
|
try:
|
|
print_pod_logs(deployment, namespace, container=container, tail=tail, since_seconds=since, follow=follow)
|
|
except Exception as e:
|
|
console.print(f"[red]Error fetching logs:[/red] {e}")
|
|
raise typer.Exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app()
|