kubsome 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- api/__init__.py +0 -0
- api/app.py +48 -0
- api/routes/__init__.py +0 -0
- api/routes/contexts.py +77 -0
- api/routes/deployments.py +62 -0
- api/routes/diagnostics.py +44 -0
- api/routes/events.py +16 -0
- api/routes/intelligence.py +63 -0
- api/routes/logs.py +36 -0
- api/routes/metrics.py +23 -0
- api/routes/operations.py +279 -0
- api/routes/overview.py +105 -0
- api/routes/pods.py +16 -0
- api/routes/terminal.py +182 -0
- api/routes/ws.py +114 -0
- api/serve.py +17 -0
- config/__init__.py +0 -0
- config/settings.py +3 -0
- core/__init__.py +0 -0
- core/ai/__init__.py +0 -0
- core/ai/anomaly.py +227 -0
- core/ai/correlation.py +124 -0
- core/ai/engine.py +646 -0
- core/ai/explain.py +180 -0
- core/ai/generator.py +129 -0
- core/ai/llm.py +102 -0
- core/ai/nlp.py +120 -0
- core/ai/playbooks.py +194 -0
- core/ai/suggest.py +49 -0
- core/analyzer.py +58 -0
- core/audit.py +47 -0
- core/banner.py +64 -0
- core/bookmarks.py +59 -0
- core/chaining.py +24 -0
- core/collectors/__init__.py +0 -0
- core/collectors/changes.py +217 -0
- core/collectors/configs.py +81 -0
- core/collectors/cost.py +318 -0
- core/collectors/deployments.py +46 -0
- core/collectors/diagnosis.py +27 -0
- core/collectors/diff.py +178 -0
- core/collectors/events.py +47 -0
- core/collectors/image_pull.py +175 -0
- core/collectors/inspect.py +173 -0
- core/collectors/jobs.py +120 -0
- core/collectors/labels.py +76 -0
- core/collectors/logs.py +224 -0
- core/collectors/metrics.py +119 -0
- core/collectors/multicluster.py +97 -0
- core/collectors/namespace.py +76 -0
- core/collectors/network.py +145 -0
- core/collectors/nodes.py +52 -0
- core/collectors/pods.py +52 -0
- core/collectors/rbac.py +94 -0
- core/collectors/rollouts.py +133 -0
- core/collectors/scaling.py +331 -0
- core/collectors/search.py +66 -0
- core/collectors/security.py +181 -0
- core/collectors/services.py +293 -0
- core/collectors/timeline.py +68 -0
- core/collectors/trace.py +231 -0
- core/commands.py +585 -0
- core/completer.py +240 -0
- core/config.py +98 -0
- core/context.py +17 -0
- core/context_formatter.py +38 -0
- core/context_switcher.py +61 -0
- core/diagnostics/__init__.py +0 -0
- core/diagnostics/engine.py +260 -0
- core/diagnostics/recommendations.py +38 -0
- core/dispatcher.py +929 -0
- core/executor.py +21 -0
- core/export.py +150 -0
- core/formatter.py +134 -0
- core/health.py +75 -0
- core/healthcheck.py +120 -0
- core/history.py +12 -0
- core/incident/__init__.py +0 -0
- core/incident/manager.py +162 -0
- core/insights.py +14 -0
- core/k8s.py +67 -0
- core/kubeconfig.py +79 -0
- core/notify.py +60 -0
- core/overview_formatter.py +162 -0
- core/plugins.py +85 -0
- core/pod_actions.py +135 -0
- core/renderers/__init__.py +0 -0
- core/renderers/ai_renderer.py +26 -0
- core/renderers/anomaly_renderer.py +135 -0
- core/renderers/changes_renderer.py +135 -0
- core/renderers/compare_renderer.py +91 -0
- core/renderers/cost_renderer.py +174 -0
- core/renderers/diagnosis_renderer.py +110 -0
- core/renderers/events_renderer.py +127 -0
- core/renderers/help_renderer.py +165 -0
- core/renderers/incident_renderer.py +92 -0
- core/renderers/inspect_renderer.py +227 -0
- core/renderers/logs_renderer.py +186 -0
- core/renderers/metrics_renderer.py +136 -0
- core/renderers/namespace_renderer.py +82 -0
- core/renderers/ops_renderer.py +230 -0
- core/renderers/rbac_renderer.py +135 -0
- core/renderers/report_renderer.py +111 -0
- core/renderers/rollout_renderer.py +123 -0
- core/renderers/scaling_renderer.py +236 -0
- core/renderers/search_renderer.py +59 -0
- core/renderers/services_renderer.py +169 -0
- core/renderers/trace_renderer.py +105 -0
- core/renderers/workflow_renderer.py +68 -0
- core/resolver.py +94 -0
- core/safety.py +16 -0
- core/selector.py +77 -0
- core/spinner.py +17 -0
- core/state.py +28 -0
- core/theme.py +68 -0
- core/watch_formatter.py +116 -0
- core/workflows.py +112 -0
- kubsome-1.0.0.dist-info/METADATA +186 -0
- kubsome-1.0.0.dist-info/RECORD +128 -0
- kubsome-1.0.0.dist-info/WHEEL +5 -0
- kubsome-1.0.0.dist-info/entry_points.txt +2 -0
- kubsome-1.0.0.dist-info/licenses/LICENSE +21 -0
- kubsome-1.0.0.dist-info/top_level.txt +6 -0
- main.py +296 -0
- plugins/__init__.py +0 -0
- plugins/example_health.py +27 -0
- tui/__init__.py +0 -0
- tui/app.py +296 -0
api/__init__.py
ADDED
|
File without changes
|
api/app.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kubsome API — FastAPI backend exposing the Kubernetes engine.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
7
|
+
from fastapi.staticfiles import StaticFiles
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from api.routes import pods, overview, contexts, events, metrics, logs, deployments, diagnostics, intelligence, terminal, operations, ws
|
|
11
|
+
|
|
12
|
+
app = FastAPI(
|
|
13
|
+
title="Kubsome API",
|
|
14
|
+
version="1.0.0",
|
|
15
|
+
description="Kubernetes Operations Engine API",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
app.add_middleware(
|
|
19
|
+
CORSMiddleware,
|
|
20
|
+
allow_origins=["http://localhost:3000", "http://localhost:3001"],
|
|
21
|
+
allow_credentials=True,
|
|
22
|
+
allow_methods=["*"],
|
|
23
|
+
allow_headers=["*"],
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
app.include_router(pods.router, prefix="/api")
|
|
27
|
+
app.include_router(overview.router, prefix="/api")
|
|
28
|
+
app.include_router(contexts.router, prefix="/api")
|
|
29
|
+
app.include_router(events.router, prefix="/api")
|
|
30
|
+
app.include_router(metrics.router, prefix="/api")
|
|
31
|
+
app.include_router(logs.router, prefix="/api")
|
|
32
|
+
app.include_router(deployments.router, prefix="/api")
|
|
33
|
+
app.include_router(diagnostics.router, prefix="/api")
|
|
34
|
+
app.include_router(intelligence.router, prefix="/api")
|
|
35
|
+
app.include_router(terminal.router, prefix="/api")
|
|
36
|
+
app.include_router(operations.router, prefix="/api")
|
|
37
|
+
app.include_router(ws.router)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.get("/health")
|
|
41
|
+
def health():
|
|
42
|
+
return {"status": "ok"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Serve Angular build in production
|
|
46
|
+
ui_dist = Path(__file__).parent.parent / "ui" / "dist" / "ui" / "browser"
|
|
47
|
+
if ui_dist.exists():
|
|
48
|
+
app.mount("/", StaticFiles(directory=str(ui_dist), html=True), name="ui")
|
api/routes/__init__.py
ADDED
|
File without changes
|
api/routes/contexts.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
from core.kubeconfig import enriched_contexts
|
|
5
|
+
from core.context_switcher import find_context, switch_context
|
|
6
|
+
from core.context import context
|
|
7
|
+
|
|
8
|
+
router = APIRouter(tags=["contexts"])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SwitchRequest(BaseModel):
|
|
12
|
+
name: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@router.get("/contexts")
|
|
16
|
+
def get_contexts():
|
|
17
|
+
return {
|
|
18
|
+
"current": context.current_context,
|
|
19
|
+
"namespace": context.namespace,
|
|
20
|
+
"contexts": enriched_contexts(),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@router.post("/switch-context")
|
|
25
|
+
def post_switch_context(req: SwitchRequest):
|
|
26
|
+
matches = find_context(req.name)
|
|
27
|
+
if not matches:
|
|
28
|
+
raise HTTPException(status_code=404, detail="No matching context")
|
|
29
|
+
|
|
30
|
+
target = matches[0]
|
|
31
|
+
switch_context(target)
|
|
32
|
+
return {
|
|
33
|
+
"switched_to": target["name"],
|
|
34
|
+
"namespace": target["namespace"],
|
|
35
|
+
"environment": target.get("environment"),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class NamespaceRequest(BaseModel):
|
|
40
|
+
namespace: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@router.get("/namespaces")
|
|
44
|
+
def get_namespaces():
|
|
45
|
+
import subprocess
|
|
46
|
+
result = subprocess.run(
|
|
47
|
+
f"kubectl --context {context.current_context} get namespaces -o jsonpath='{{.items[*].metadata.name}}'",
|
|
48
|
+
shell=True, capture_output=True, text=True,
|
|
49
|
+
)
|
|
50
|
+
if result.returncode != 0:
|
|
51
|
+
return {"namespaces": [], "current": context.namespace}
|
|
52
|
+
raw = result.stdout.strip().strip("'")
|
|
53
|
+
namespaces = sorted([ns for ns in raw.split() if ns])
|
|
54
|
+
return {"namespaces": namespaces, "current": context.namespace}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@router.post("/switch-namespace")
|
|
58
|
+
def post_switch_namespace(req: NamespaceRequest):
|
|
59
|
+
from core.state import save_state
|
|
60
|
+
context.namespace = req.namespace
|
|
61
|
+
save_state(context.current_context, context.namespace)
|
|
62
|
+
return {"namespace": context.namespace}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@router.get("/namespaces/{ctx}")
|
|
66
|
+
def get_namespaces_for(ctx: str):
|
|
67
|
+
"""Get namespaces for a specific context without switching global state."""
|
|
68
|
+
import subprocess
|
|
69
|
+
result = subprocess.run(
|
|
70
|
+
f"kubectl --context {ctx} get namespaces -o jsonpath='{{.items[*].metadata.name}}'",
|
|
71
|
+
shell=True, capture_output=True, text=True,
|
|
72
|
+
)
|
|
73
|
+
if result.returncode != 0:
|
|
74
|
+
return {"namespaces": []}
|
|
75
|
+
raw = result.stdout.strip().strip("'")
|
|
76
|
+
namespaces = sorted([ns for ns in raw.split() if ns])
|
|
77
|
+
return {"namespaces": namespaces}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
from core.context import context
|
|
5
|
+
from core.collectors.deployments import collect_deployments
|
|
6
|
+
from core.collectors.rollouts import (
|
|
7
|
+
rollout_status, rollout_history,
|
|
8
|
+
rollout_rollback, rollout_restart,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
import subprocess
|
|
12
|
+
|
|
13
|
+
router = APIRouter(tags=["deployments"])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ScaleRequest(BaseModel):
|
|
17
|
+
replicas: int
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@router.get("/deployments")
|
|
21
|
+
def get_deployments():
|
|
22
|
+
return {
|
|
23
|
+
"context": context.current_context,
|
|
24
|
+
"namespace": context.namespace,
|
|
25
|
+
"deployments": collect_deployments(),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@router.get("/rollout/{name}")
|
|
30
|
+
def get_rollout(name: str):
|
|
31
|
+
status = rollout_status(name)
|
|
32
|
+
history = rollout_history(name)
|
|
33
|
+
return {"name": name, "status": status, "history": history}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@router.post("/restart/{name}")
|
|
37
|
+
def post_restart(name: str):
|
|
38
|
+
success, output = rollout_restart(name)
|
|
39
|
+
if not success:
|
|
40
|
+
raise HTTPException(status_code=500, detail="Restart failed")
|
|
41
|
+
return {"restarted": name}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@router.post("/rollback/{name}")
|
|
45
|
+
def post_rollback(name: str):
|
|
46
|
+
success, output = rollout_rollback(name)
|
|
47
|
+
if not success:
|
|
48
|
+
raise HTTPException(status_code=500, detail="Rollback failed")
|
|
49
|
+
return {"rolled_back": name}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@router.post("/scale/{name}")
|
|
53
|
+
def post_scale(name: str, req: ScaleRequest):
|
|
54
|
+
cmd = (
|
|
55
|
+
f"kubectl --context {context.current_context} "
|
|
56
|
+
f"scale deployment/{name} "
|
|
57
|
+
f"--replicas={req.replicas} -n {context.namespace}"
|
|
58
|
+
)
|
|
59
|
+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
|
60
|
+
if result.returncode != 0:
|
|
61
|
+
raise HTTPException(status_code=500, detail=result.stderr.strip())
|
|
62
|
+
return {"scaled": name, "replicas": req.replicas}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
|
|
3
|
+
from core.context import context
|
|
4
|
+
from core.collectors.inspect import inspect_pod, pod_events, extract_pod_details
|
|
5
|
+
from core.collectors.diagnosis import collect_diagnosis
|
|
6
|
+
from core.collectors.trace import trace_resource
|
|
7
|
+
from core.diagnostics.engine import diagnose
|
|
8
|
+
from core.diagnostics.recommendations import recommend
|
|
9
|
+
|
|
10
|
+
router = APIRouter(tags=["diagnostics"])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.get("/inspect/{pod}")
|
|
14
|
+
def get_inspect(pod: str):
|
|
15
|
+
pod_data = inspect_pod(pod)
|
|
16
|
+
if not pod_data:
|
|
17
|
+
raise HTTPException(status_code=404, detail="Pod not found")
|
|
18
|
+
details = extract_pod_details(pod_data)
|
|
19
|
+
events = pod_events(pod)
|
|
20
|
+
recommendation = recommend(pod_data)
|
|
21
|
+
return {
|
|
22
|
+
"pod": pod,
|
|
23
|
+
"details": details,
|
|
24
|
+
"events": events,
|
|
25
|
+
"recommendation": recommendation,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@router.get("/diagnose/{pod}")
|
|
30
|
+
def get_diagnose(pod: str):
|
|
31
|
+
data = collect_diagnosis(pod)
|
|
32
|
+
if not data:
|
|
33
|
+
raise HTTPException(status_code=404, detail="Pod not found")
|
|
34
|
+
findings = diagnose(data)
|
|
35
|
+
return {
|
|
36
|
+
"pod": pod,
|
|
37
|
+
"findings": findings,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@router.get("/trace/{name}")
|
|
42
|
+
def get_trace(name: str):
|
|
43
|
+
data = trace_resource(name)
|
|
44
|
+
return {"name": name, "trace": data}
|
api/routes/events.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
|
|
3
|
+
from core.collectors.events import collect_events
|
|
4
|
+
from core.context import context
|
|
5
|
+
|
|
6
|
+
router = APIRouter(tags=["events"])
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@router.get("/events")
|
|
10
|
+
def get_events(limit: int = 50):
|
|
11
|
+
events = collect_events(limit=limit)
|
|
12
|
+
return {
|
|
13
|
+
"context": context.current_context,
|
|
14
|
+
"namespace": context.namespace,
|
|
15
|
+
"events": events,
|
|
16
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from fastapi import APIRouter, Query
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
from core.collectors.search import search_resources
|
|
5
|
+
from core.collectors.security import security_scan
|
|
6
|
+
from core.collectors.cost import resource_recommendations, find_unused_resources
|
|
7
|
+
from core.healthcheck import run_health_check
|
|
8
|
+
from core.ai.engine import handle_ai_query
|
|
9
|
+
from core.ai.anomaly import detect_anomalies
|
|
10
|
+
|
|
11
|
+
router = APIRouter(tags=["intelligence"])
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AiRequest(BaseModel):
|
|
15
|
+
query: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.get("/search")
|
|
19
|
+
def get_search(q: str = Query(..., min_length=1)):
|
|
20
|
+
results = search_resources(q)
|
|
21
|
+
return {"query": q, "results": results}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@router.get("/security")
|
|
25
|
+
def get_security():
|
|
26
|
+
findings = security_scan()
|
|
27
|
+
return {"findings": findings}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@router.get("/health-check")
|
|
31
|
+
def get_health_check():
|
|
32
|
+
return run_health_check()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@router.get("/anomalies")
|
|
36
|
+
def get_anomalies():
|
|
37
|
+
alerts = detect_anomalies()
|
|
38
|
+
return {"alerts": alerts}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@router.get("/optimize")
|
|
42
|
+
def get_optimize():
|
|
43
|
+
recs = resource_recommendations()
|
|
44
|
+
return {"recommendations": recs}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@router.get("/unused")
|
|
48
|
+
def get_unused():
|
|
49
|
+
return {"resources": find_unused_resources()}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@router.post("/ai")
|
|
53
|
+
def post_ai(req: AiRequest):
|
|
54
|
+
import re
|
|
55
|
+
response = handle_ai_query(req.query)
|
|
56
|
+
# Strip Rich markup tags for API consumers
|
|
57
|
+
content = response.get("content", "")
|
|
58
|
+
content = re.sub(r'\[/?[^\]]+\]', '', content)
|
|
59
|
+
return {
|
|
60
|
+
"title": response.get("title", "").replace("🤖 ", ""),
|
|
61
|
+
"answer": content,
|
|
62
|
+
"severity": response.get("severity", "info"),
|
|
63
|
+
}
|
api/routes/logs.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from fastapi import APIRouter, Query
|
|
2
|
+
from fastapi.responses import StreamingResponse
|
|
3
|
+
|
|
4
|
+
from core.context import context
|
|
5
|
+
from core.collectors.logs import fetch_logs, stream_logs
|
|
6
|
+
|
|
7
|
+
router = APIRouter(tags=["logs"])
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@router.get("/logs/{pod}")
|
|
11
|
+
def get_logs(
|
|
12
|
+
pod: str,
|
|
13
|
+
tail: int = Query(100, ge=1, le=5000),
|
|
14
|
+
errors: bool = False,
|
|
15
|
+
previous: bool = False,
|
|
16
|
+
):
|
|
17
|
+
lines = fetch_logs(pod, tail=tail, previous=previous, errors_only=errors)
|
|
18
|
+
return {
|
|
19
|
+
"pod": pod,
|
|
20
|
+
"namespace": context.namespace,
|
|
21
|
+
"lines": lines,
|
|
22
|
+
"count": len(lines),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@router.get("/logs/{pod}/stream")
|
|
27
|
+
def get_logs_stream(pod: str):
|
|
28
|
+
def generate():
|
|
29
|
+
process = stream_logs(pod)
|
|
30
|
+
try:
|
|
31
|
+
for line in process.stdout:
|
|
32
|
+
yield line
|
|
33
|
+
finally:
|
|
34
|
+
process.kill()
|
|
35
|
+
|
|
36
|
+
return StreamingResponse(generate(), media_type="text/plain")
|
api/routes/metrics.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
|
|
3
|
+
from core.collectors.metrics import top_pods, top_nodes
|
|
4
|
+
from core.context import context
|
|
5
|
+
|
|
6
|
+
router = APIRouter(tags=["metrics"])
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@router.get("/top/pods")
|
|
10
|
+
def get_top_pods():
|
|
11
|
+
return {
|
|
12
|
+
"context": context.current_context,
|
|
13
|
+
"namespace": context.namespace,
|
|
14
|
+
"pods": top_pods(),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.get("/top/nodes")
|
|
19
|
+
def get_top_nodes():
|
|
20
|
+
return {
|
|
21
|
+
"context": context.current_context,
|
|
22
|
+
"nodes": top_nodes(),
|
|
23
|
+
}
|
api/routes/operations.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from fastapi import APIRouter, Query
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from core.context import context
|
|
7
|
+
|
|
8
|
+
router = APIRouter(tags=["operations"])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ─── Image Pull Secrets ───────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
@router.get("/image-pull-secrets")
|
|
14
|
+
def get_image_pull_secrets(pod: Optional[str] = None):
|
|
15
|
+
from core.collectors.image_pull import check_image_pull_secrets
|
|
16
|
+
return check_image_pull_secrets(pod)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ─── Incident Mode ───────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
class IncidentStartRequest(BaseModel):
|
|
22
|
+
title: str = ""
|
|
23
|
+
|
|
24
|
+
class NoteRequest(BaseModel):
|
|
25
|
+
text: str
|
|
26
|
+
|
|
27
|
+
@router.post("/incident/start")
|
|
28
|
+
def incident_start(req: IncidentStartRequest):
|
|
29
|
+
from core.incident.manager import start_incident
|
|
30
|
+
incident = start_incident(req.title)
|
|
31
|
+
return incident
|
|
32
|
+
|
|
33
|
+
@router.post("/incident/stop")
|
|
34
|
+
def incident_stop():
|
|
35
|
+
from core.incident.manager import stop_incident
|
|
36
|
+
result = stop_incident()
|
|
37
|
+
if not result:
|
|
38
|
+
return {"status": "no active incident"}
|
|
39
|
+
incident, path = result
|
|
40
|
+
return {"incident": incident, "export_path": path}
|
|
41
|
+
|
|
42
|
+
@router.get("/incident/status")
|
|
43
|
+
def incident_status():
|
|
44
|
+
from core.incident.manager import get_active
|
|
45
|
+
return get_active() or {"status": "no active incident"}
|
|
46
|
+
|
|
47
|
+
@router.post("/incident/note")
|
|
48
|
+
def incident_note(req: NoteRequest):
|
|
49
|
+
from core.incident.manager import add_note
|
|
50
|
+
if add_note(req.text):
|
|
51
|
+
return {"added": True}
|
|
52
|
+
return {"added": False, "reason": "no active incident"}
|
|
53
|
+
|
|
54
|
+
@router.post("/incident/snapshot")
|
|
55
|
+
def incident_snapshot():
|
|
56
|
+
from core.incident.manager import snapshot
|
|
57
|
+
if snapshot():
|
|
58
|
+
return {"captured": True}
|
|
59
|
+
return {"captured": False, "reason": "no active incident"}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ─── RBAC ─────────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
@router.get("/rbac")
|
|
65
|
+
def get_rbac():
|
|
66
|
+
from core.collectors.rbac import list_role_bindings
|
|
67
|
+
return {"bindings": list_role_bindings()}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ─── CronJobs / Jobs ─────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
@router.get("/cronjobs")
|
|
73
|
+
def get_cronjobs():
|
|
74
|
+
from core.collectors.jobs import list_cronjobs
|
|
75
|
+
return {"cronjobs": list_cronjobs()}
|
|
76
|
+
|
|
77
|
+
@router.get("/jobs")
|
|
78
|
+
def get_jobs():
|
|
79
|
+
from core.collectors.jobs import list_jobs
|
|
80
|
+
return {"jobs": list_jobs()}
|
|
81
|
+
|
|
82
|
+
@router.post("/trigger/{name}")
|
|
83
|
+
def trigger_cronjob(name: str):
|
|
84
|
+
from core.collectors.jobs import trigger_cronjob as do_trigger
|
|
85
|
+
success, output = do_trigger(name)
|
|
86
|
+
return {"success": success, "output": output}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ─── Namespace Overview ───────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
@router.get("/ns-overview")
|
|
92
|
+
def get_ns_overview():
|
|
93
|
+
from core.collectors.namespace import namespace_summary
|
|
94
|
+
return namespace_summary()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ─── Compare (Multi-cluster) ─────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
class CompareRequest(BaseModel):
|
|
100
|
+
ctx_a: str
|
|
101
|
+
ctx_b: str
|
|
102
|
+
ns_a: str = "default"
|
|
103
|
+
ns_b: str = "default"
|
|
104
|
+
|
|
105
|
+
@router.post("/compare")
|
|
106
|
+
def post_compare(req: CompareRequest):
|
|
107
|
+
from core.collectors.multicluster import compare_contexts
|
|
108
|
+
data = compare_contexts(req.ctx_a, req.ctx_b, req.ns_a, req.ns_b)
|
|
109
|
+
return data
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ─── Network ─────────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
@router.get("/netcheck/{pod}")
|
|
115
|
+
def get_netcheck(pod: str):
|
|
116
|
+
from core.collectors.network import netcheck
|
|
117
|
+
return netcheck(pod)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ─── ConfigMap / Secret ───────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
@router.get("/configmap/{name}")
|
|
123
|
+
def get_configmap(name: str):
|
|
124
|
+
from core.collectors.configs import get_configmap as fetch_cm
|
|
125
|
+
return fetch_cm(name)
|
|
126
|
+
|
|
127
|
+
@router.get("/secret/{name}")
|
|
128
|
+
def get_secret(name: str):
|
|
129
|
+
from core.collectors.configs import get_secret as fetch_secret
|
|
130
|
+
return fetch_secret(name)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ─── Deployment Diff ──────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
@router.get("/diff/{name}")
|
|
136
|
+
def get_diff(name: str):
|
|
137
|
+
from core.collectors.diff import deployment_diff
|
|
138
|
+
return deployment_diff(name)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ─── Scaling: HPA, PDB, Capacity, Quota, Drain ───────────────────────────────
|
|
142
|
+
|
|
143
|
+
@router.get("/hpa")
|
|
144
|
+
def get_hpa():
|
|
145
|
+
from core.collectors.scaling import list_hpa
|
|
146
|
+
return {"hpa": list_hpa()}
|
|
147
|
+
|
|
148
|
+
@router.get("/pdb")
|
|
149
|
+
def get_pdb():
|
|
150
|
+
from core.collectors.scaling import list_pdb
|
|
151
|
+
return {"pdb": list_pdb()}
|
|
152
|
+
|
|
153
|
+
@router.get("/capacity")
|
|
154
|
+
def get_capacity():
|
|
155
|
+
from core.collectors.scaling import cluster_capacity
|
|
156
|
+
return cluster_capacity()
|
|
157
|
+
|
|
158
|
+
@router.get("/quota")
|
|
159
|
+
def get_quota():
|
|
160
|
+
from core.collectors.scaling import namespace_quota
|
|
161
|
+
return namespace_quota()
|
|
162
|
+
|
|
163
|
+
@router.get("/drain-check/{node}")
|
|
164
|
+
def get_drain_check(node: str):
|
|
165
|
+
from core.collectors.scaling import drain_check
|
|
166
|
+
return drain_check(node)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ─── Timeline ────────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
@router.get("/timeline")
|
|
172
|
+
def get_timeline(minutes: int = 60):
|
|
173
|
+
from core.collectors.timeline import build_timeline
|
|
174
|
+
return {"events": build_timeline(minutes=minutes)}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ─── Ingress / Mesh / Dependencies / DNS ──────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
@router.get("/ingress")
|
|
180
|
+
def get_ingress():
|
|
181
|
+
from core.collectors.services import list_ingresses
|
|
182
|
+
return {"ingresses": list_ingresses()}
|
|
183
|
+
|
|
184
|
+
@router.get("/mesh")
|
|
185
|
+
def get_mesh():
|
|
186
|
+
from core.collectors.services import detect_mesh
|
|
187
|
+
return detect_mesh()
|
|
188
|
+
|
|
189
|
+
@router.get("/deps/{name}")
|
|
190
|
+
def get_deps(name: str):
|
|
191
|
+
from core.collectors.services import service_dependencies
|
|
192
|
+
return service_dependencies(name)
|
|
193
|
+
|
|
194
|
+
@router.get("/dns/{service}")
|
|
195
|
+
def get_dns(service: str):
|
|
196
|
+
from core.collectors.services import dns_debug
|
|
197
|
+
return dns_debug(service)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ─── Correlate / Playbook ─────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
@router.get("/correlate")
|
|
203
|
+
def get_correlate(target: Optional[str] = None):
|
|
204
|
+
from core.ai.correlation import correlate
|
|
205
|
+
return {"chains": correlate(target)}
|
|
206
|
+
|
|
207
|
+
@router.get("/playbook/{issue}")
|
|
208
|
+
def get_playbook(issue: str):
|
|
209
|
+
from core.ai.playbooks import get_playbook
|
|
210
|
+
return get_playbook(issue)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ─── Changelog / Snap ─────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
@router.get("/changelog")
|
|
216
|
+
def get_changelog():
|
|
217
|
+
from core.collectors.changes import build_changelog
|
|
218
|
+
return {"changelog": build_changelog()}
|
|
219
|
+
|
|
220
|
+
@router.post("/snap")
|
|
221
|
+
def post_snap():
|
|
222
|
+
from core.collectors.changes import take_state_snapshot
|
|
223
|
+
path = take_state_snapshot()
|
|
224
|
+
return {"path": path}
|
|
225
|
+
|
|
226
|
+
@router.get("/snap-diff")
|
|
227
|
+
def get_snap_diff():
|
|
228
|
+
from core.collectors.changes import get_latest_snapshot, diff_snapshots
|
|
229
|
+
old = get_latest_snapshot()
|
|
230
|
+
return diff_snapshots(old)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ─── Labels ───────────────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
@router.get("/labels/{resource_type}")
|
|
236
|
+
def get_labels(resource_type: str, name: Optional[str] = None):
|
|
237
|
+
from core.collectors.labels import get_labels as fetch_labels
|
|
238
|
+
return {"resources": fetch_labels(resource_type, name)}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ─── Audit ────────────────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
@router.get("/audit")
|
|
244
|
+
def get_audit():
|
|
245
|
+
from core.audit import get_audit_log
|
|
246
|
+
return {"log": get_audit_log()}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# ─── Export ───────────────────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
@router.get("/export")
|
|
252
|
+
def get_export(format: str = "md"):
|
|
253
|
+
from core.export import export_report
|
|
254
|
+
path = export_report(format=format)
|
|
255
|
+
return {"path": path, "format": format}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ─── Explain / Generate ───────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
class ExplainRequest(BaseModel):
|
|
261
|
+
query: str
|
|
262
|
+
|
|
263
|
+
class GenerateRequest(BaseModel):
|
|
264
|
+
kind: str
|
|
265
|
+
name: str
|
|
266
|
+
|
|
267
|
+
@router.post("/explain")
|
|
268
|
+
def post_explain(req: ExplainRequest):
|
|
269
|
+
from core.ai.explain import explain
|
|
270
|
+
result = explain(req.query)
|
|
271
|
+
# Strip Rich markup
|
|
272
|
+
content = re.sub(r'\[/?[^\]]+\]', '', result.get("content", ""))
|
|
273
|
+
return {"title": result.get("title", ""), "content": content}
|
|
274
|
+
|
|
275
|
+
@router.post("/generate")
|
|
276
|
+
def post_generate(req: GenerateRequest):
|
|
277
|
+
from core.ai.generator import generate_manifest
|
|
278
|
+
yaml_output = generate_manifest(req.kind, req.name, context.namespace)
|
|
279
|
+
return {"yaml": yaml_output or ""}
|