Deploy HF RL Explorer
Browse files- app/admin_app.py +20 -0
- scripts/deploy_spaces.py +1 -1
- scripts/schedule_indexer.py +13 -3
- tests/test_security.py +7 -0
app/admin_app.py
CHANGED
|
@@ -42,6 +42,26 @@ def _startup() -> None:
|
|
| 42 |
threading.Thread(target=lambda: catalog.environments(include_hidden=True), daemon=True, name="warm").start()
|
| 43 |
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
@app.get("/api/me")
|
| 46 |
def me(request: Request):
|
| 47 |
u = auth.current_user(request)
|
|
|
|
| 42 |
threading.Thread(target=lambda: catalog.environments(include_hidden=True), daemon=True, name="warm").start()
|
| 43 |
|
| 44 |
|
| 45 |
+
@app.get("/healthz", include_in_schema=False)
|
| 46 |
+
def healthz():
|
| 47 |
+
"""Alive: the process answers."""
|
| 48 |
+
return {"ok": True}
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@app.get("/readyz", include_in_schema=False)
|
| 52 |
+
def readyz():
|
| 53 |
+
"""Ready: the bucket it keeps the settings in is mounted and writable (this app runs no rollouts or indexes)."""
|
| 54 |
+
from fastapi.responses import JSONResponse
|
| 55 |
+
|
| 56 |
+
try:
|
| 57 |
+
settings.DIR.mkdir(parents=True, exist_ok=True)
|
| 58 |
+
(settings.DIR / ".ready").write_text("ok")
|
| 59 |
+
store = "ok"
|
| 60 |
+
except OSError as e:
|
| 61 |
+
store = f"unwritable: {type(e).__name__}"
|
| 62 |
+
return JSONResponse({"ok": store == "ok", "store": store}, status_code=200 if store == "ok" else 503)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
@app.get("/api/me")
|
| 66 |
def me(request: Request):
|
| 67 |
u = auth.current_user(request)
|
scripts/deploy_spaces.py
CHANGED
|
@@ -36,7 +36,7 @@ SPACES = {
|
|
| 36 |
},
|
| 37 |
"admin": {
|
| 38 |
"repo": "FineEnvs/RL-Explorer-admin", "private": True, "readme": "ADMIN_SPACE_README.md", "hardware": "cpu-basic",
|
| 39 |
-
"variables": {"RLX_APP": "app.admin_app:app", "RLX_OAUTH_SCOPES": "openid profile", "RLX_EXPLORER_URL": EXPLORER_URL},
|
| 40 |
"volumes": [DATA],
|
| 41 |
},
|
| 42 |
}
|
|
|
|
| 36 |
},
|
| 37 |
"admin": {
|
| 38 |
"repo": "FineEnvs/RL-Explorer-admin", "private": True, "readme": "ADMIN_SPACE_README.md", "hardware": "cpu-basic",
|
| 39 |
+
"variables": {"RLX_APP": "app.admin_app:app", "RLX_OAUTH_SCOPES": "openid profile read-memberships", "RLX_EXPLORER_URL": EXPLORER_URL},
|
| 40 |
"volumes": [DATA],
|
| 41 |
},
|
| 42 |
}
|
scripts/schedule_indexer.py
CHANGED
|
@@ -52,7 +52,8 @@ APP_DIR = "/home/user/app" # the Dockerfile's WORKDIR
|
|
| 52 |
|
| 53 |
def spec(args: argparse.Namespace) -> dict:
|
| 54 |
run = (f"cd {APP_DIR} && exec uv run --frozen --no-dev python -m app.indexer --store /data "
|
| 55 |
-
f"--budget-min {args.budget_min:g} --max-builds {args.max_builds} --keep {args.keep}
|
|
|
|
| 56 |
return {
|
| 57 |
"namespace": args.namespace,
|
| 58 |
"name": args.name,
|
|
@@ -63,7 +64,9 @@ def spec(args: argparse.Namespace) -> dict:
|
|
| 63 |
"flavor": args.flavor,
|
| 64 |
"timeout": args.timeout,
|
| 65 |
"env": {"STORAGE_DIR": "/data", "RLX_CACHE_DIR": "/tmp/rlx-cache", "HF_HOME": "/tmp/hf-home", "PYTHONUNBUFFERED": "1"},
|
| 66 |
-
|
|
|
|
|
|
|
| 67 |
"volumes": [{"type": "bucket", "source": args.bucket, "mount_path": "/data"}],
|
| 68 |
"labels": {"app": "rl-explorer", "role": "indexer"},
|
| 69 |
}
|
|
@@ -87,10 +90,16 @@ def cli(s: dict) -> str:
|
|
| 87 |
def apply(s: dict, replace: bool) -> int:
|
| 88 |
from huggingface_hub import HfApi, Volume, get_token
|
| 89 |
|
|
|
|
|
|
|
| 90 |
token = get_token()
|
| 91 |
if not token:
|
| 92 |
print("No HF token: run `hf auth login` (a token that can create Jobs in the FineEnvs organization).", file=sys.stderr)
|
| 93 |
return 2
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
api = HfApi(token=token)
|
| 95 |
existing = [j for j in api.list_scheduled_jobs(namespace=s["namespace"], labels={"name": s["name"]})]
|
| 96 |
if existing and not replace:
|
|
@@ -102,7 +111,7 @@ def apply(s: dict, replace: bool) -> int:
|
|
| 102 |
print(f"deleted scheduled Job {j.id}")
|
| 103 |
job = api.create_scheduled_job(
|
| 104 |
image=s["image"], command=s["command"], schedule=s["schedule"], concurrency=s["concurrency"], env=s["env"],
|
| 105 |
-
secrets={k:
|
| 106 |
labels=s["labels"], volumes=[Volume(**v) for v in s["volumes"]], namespace=s["namespace"])
|
| 107 |
print(f"created scheduled Job {job.id} ({s['schedule']}); runs: hf jobs scheduled inspect {job.id} --namespace {s['namespace']}")
|
| 108 |
return 0
|
|
@@ -112,6 +121,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
| 112 |
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 113 |
ap.add_argument("--apply", action="store_true", help="create the scheduled Job (otherwise only print it)")
|
| 114 |
ap.add_argument("--replace", action="store_true", help="with --apply: delete a scheduled Job of the same name first")
|
|
|
|
| 115 |
ap.add_argument("--namespace", default=NAMESPACE)
|
| 116 |
ap.add_argument("--name", default=NAME)
|
| 117 |
ap.add_argument("--image", default=IMAGE, help="a Docker image with this app: the explorer Space's, by default")
|
|
|
|
| 52 |
|
| 53 |
def spec(args: argparse.Namespace) -> dict:
|
| 54 |
run = (f"cd {APP_DIR} && exec uv run --frozen --no-dev python -m app.indexer --store /data "
|
| 55 |
+
f"--budget-min {args.budget_min:g} --max-builds {args.max_builds} --keep {args.keep}"
|
| 56 |
+
+ (" --use-token" if args.with_token else ""))
|
| 57 |
return {
|
| 58 |
"namespace": args.namespace,
|
| 59 |
"name": args.name,
|
|
|
|
| 64 |
"flavor": args.flavor,
|
| 65 |
"timeout": args.timeout,
|
| 66 |
"env": {"STORAGE_DIR": "/data", "RLX_CACHE_DIR": "/tmp/rlx-cache", "HF_HOME": "/tmp/hf-home", "PYTHONUNBUFFERED": "1"},
|
| 67 |
+
# anonymous by default: public datasets need no token. --with-token sends RLX_INDEXER_TOKEN (a read-only token
|
| 68 |
+
# made for this) as HF_TOKEN, for the Hub's higher rate limits; never your login token, which can write
|
| 69 |
+
"secrets": ["HF_TOKEN"] if args.with_token else [],
|
| 70 |
"volumes": [{"type": "bucket", "source": args.bucket, "mount_path": "/data"}],
|
| 71 |
"labels": {"app": "rl-explorer", "role": "indexer"},
|
| 72 |
}
|
|
|
|
| 90 |
def apply(s: dict, replace: bool) -> int:
|
| 91 |
from huggingface_hub import HfApi, Volume, get_token
|
| 92 |
|
| 93 |
+
import os
|
| 94 |
+
|
| 95 |
token = get_token()
|
| 96 |
if not token:
|
| 97 |
print("No HF token: run `hf auth login` (a token that can create Jobs in the FineEnvs organization).", file=sys.stderr)
|
| 98 |
return 2
|
| 99 |
+
job_token = os.environ.get("RLX_INDEXER_TOKEN")
|
| 100 |
+
if s["secrets"] and not job_token:
|
| 101 |
+
print("--with-token needs RLX_INDEXER_TOKEN: a read-only token made for the indexer (not your login token).", file=sys.stderr)
|
| 102 |
+
return 2
|
| 103 |
api = HfApi(token=token)
|
| 104 |
existing = [j for j in api.list_scheduled_jobs(namespace=s["namespace"], labels={"name": s["name"]})]
|
| 105 |
if existing and not replace:
|
|
|
|
| 111 |
print(f"deleted scheduled Job {j.id}")
|
| 112 |
job = api.create_scheduled_job(
|
| 113 |
image=s["image"], command=s["command"], schedule=s["schedule"], concurrency=s["concurrency"], env=s["env"],
|
| 114 |
+
secrets={k: job_token for k in s["secrets"]} or None, flavor=s["flavor"], timeout=s["timeout"], name=s["name"],
|
| 115 |
labels=s["labels"], volumes=[Volume(**v) for v in s["volumes"]], namespace=s["namespace"])
|
| 116 |
print(f"created scheduled Job {job.id} ({s['schedule']}); runs: hf jobs scheduled inspect {job.id} --namespace {s['namespace']}")
|
| 117 |
return 0
|
|
|
|
| 121 |
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 122 |
ap.add_argument("--apply", action="store_true", help="create the scheduled Job (otherwise only print it)")
|
| 123 |
ap.add_argument("--replace", action="store_true", help="with --apply: delete a scheduled Job of the same name first")
|
| 124 |
+
ap.add_argument("--with-token", action="store_true", help="send RLX_INDEXER_TOKEN (a read-only token) as the Job's HF_TOKEN")
|
| 125 |
ap.add_argument("--namespace", default=NAMESPACE)
|
| 126 |
ap.add_argument("--name", default=NAME)
|
| 127 |
ap.add_argument("--image", default=IMAGE, help="a Docker image with this app: the explorer Space's, by default")
|
tests/test_security.py
CHANGED
|
@@ -545,3 +545,10 @@ def test_a_rollout_never_downloads_reference_solutions(monkeypatch):
|
|
| 545 |
for f in ("tasks/t1/solution/solve.sh", "tasks/t1/steps/s1/solution/solve.sh", "tasks/t1/steps/two/solution/deep/x.py"):
|
| 546 |
assert any(fnmatch(f, p) for p in seen["ignore_patterns"]), f
|
| 547 |
assert not any(fnmatch("tasks/t1/steps/s1/instruction.md", p) for p in seen["ignore_patterns"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 545 |
for f in ("tasks/t1/solution/solve.sh", "tasks/t1/steps/s1/solution/solve.sh", "tasks/t1/steps/two/solution/deep/x.py"):
|
| 546 |
assert any(fnmatch(f, p) for p in seen["ignore_patterns"]), f
|
| 547 |
assert not any(fnmatch("tasks/t1/steps/s1/instruction.md", p) for p in seen["ignore_patterns"])
|
| 548 |
+
|
| 549 |
+
|
| 550 |
+
def test_the_admin_space_has_health_checks_and_nothing_else_open():
|
| 551 |
+
assert admin.get("/healthz").json() == {"ok": True}
|
| 552 |
+
r = admin.get("/readyz")
|
| 553 |
+
assert r.status_code == 200 and r.json()["store"] == "ok"
|
| 554 |
+
assert admin.get("/api/admin/settings").status_code == 401
|