-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmodal_app.py
More file actions
107 lines (84 loc) · 3.26 KB
/
Copy pathmodal_app.py
File metadata and controls
107 lines (84 loc) · 3.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import os
import modal
app = modal.App("rootstock-admin")
app.image = modal.Image.debian_slim().pip_install("fastapi[standard]")
env = os.environ.get("MODAL_ENVIRONMENT", "dev")
vol = modal.Volume.from_name(f"rootstock-admin-{env}", create_if_missing=True)
@app.function(volumes={"/data": vol})
@modal.fastapi_endpoint(method="POST", requires_proxy_auth=True)
def manifest(manifest: dict):
import json
vol.reload()
name = manifest.get("cluster")
with open(f"/data/{name}.json", "w") as f:
json.dump(manifest, f)
vol.commit()
return manifest
@app.function(volumes={"/data": vol})
@modal.fastapi_endpoint(method="POST", requires_proxy_auth=True)
def usage(payload: dict):
"""Ingest a cluster's usage rollups: {"cluster": str, "rows": [...]}.
Rows are the aggregated rollup rows produced by `rootstock usage report`
(keyed by month/cluster/env/checkpoint/device/client, with sessions,
n_calculations, duration_s, unique_users). Rows are stored per month and
a push only replaces the months it contains, so history survives even if
a cluster's spool loses old months (e.g. scratch purge policies).
"""
import json
import os
import re
from collections import defaultdict
from fastapi import HTTPException
cluster = payload.get("cluster")
rows = payload.get("rows")
if not isinstance(cluster, str) or not re.fullmatch(r"[A-Za-z0-9_-]+", cluster):
raise HTTPException(422, "invalid or missing 'cluster'")
if not isinstance(rows, list):
raise HTTPException(422, "'rows' must be a list of rollup rows")
by_month = defaultdict(list)
for row in rows:
if not isinstance(row, dict) or not re.fullmatch(
r"\d{4}-\d{2}", str(row.get("month"))
):
raise HTTPException(422, "every row needs a 'month' of the form YYYY-MM")
# Only derived counts ever leave a cluster; drop user hashes if a
# client ever sends them by mistake.
row.pop("users", None)
by_month[row["month"]].append(row)
vol.reload()
os.makedirs(f"/data/usage/{cluster}", exist_ok=True)
for month, month_rows in by_month.items():
with open(f"/data/usage/{cluster}/{month}.json", "w") as f:
json.dump({"cluster": cluster, "month": month, "rows": month_rows}, f)
vol.commit()
return {"cluster": cluster, "months": sorted(by_month)}
@app.function(volumes={"/data": vol}, min_containers=1)
@modal.asgi_app()
def dashboard():
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
web_app = FastAPI()
web_app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["GET"],
allow_headers=["*"],
)
@web_app.get("/")
def get_manifests():
import json
from pathlib import Path
vol.reload()
manifests = [json.loads(p.read_text()) for p in Path("/data").glob("*.json")]
return {"manifests": manifests}
@web_app.get("/usage")
def get_usage():
import json
from pathlib import Path
vol.reload()
rollups = [
json.loads(p.read_text())
for p in sorted(Path("/data/usage").glob("*/*.json"))
]
return {"usage": rollups}
return web_app