mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-31 12:19:48 +02:00
[eric] runner: per-app VM image boots a published bundle from storage and serves backend + frontend from one process (ENG-293)
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
"""Boot for a published app's runner VM (ENG-293). Downloads the app's bundle from storage by
|
||||
APP_SLUG (read-only creds scoped to apps/*), unpacks it, installs the app's own requirements into
|
||||
a local target dir, then serves backend + built frontend from one uvicorn on :8080. BUNDLE_VERSION
|
||||
in env busts the on-disk copy on republish (the cloud stamps a fresh value into the machine)."""
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
|
||||
import boto3
|
||||
from botocore.config import Config
|
||||
|
||||
WORKDIR = "/data/app"
|
||||
SLUG = os.environ.get("APP_SLUG", "")
|
||||
VERSION = os.environ.get("BUNDLE_VERSION", "0")
|
||||
BUCKET = os.environ.get("TIGRIS_BUCKET", "openswarm-app-bundles")
|
||||
STAMP = os.path.join(WORKDIR, ".bundle-version")
|
||||
|
||||
|
||||
def s3():
|
||||
return boto3.client(
|
||||
"s3",
|
||||
endpoint_url=os.environ.get("AWS_ENDPOINT_URL_S3", "https://fly.storage.tigris.dev"),
|
||||
region_name=os.environ.get("TIGRIS_REGION", "auto"),
|
||||
aws_access_key_id=os.environ.get("TIGRIS_ACCESS_KEY_ID", ""),
|
||||
aws_secret_access_key=os.environ.get("TIGRIS_SECRET_ACCESS_KEY", ""),
|
||||
config=Config(signature_version="s3v4"),
|
||||
)
|
||||
|
||||
|
||||
def fetch_and_unpack() -> None:
|
||||
obj = s3().get_object(Bucket=BUCKET, Key=f"apps/{SLUG}/bundle.tar.gz")
|
||||
data = obj["Body"].read()
|
||||
os.makedirs(WORKDIR, exist_ok=True)
|
||||
with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as tar:
|
||||
for m in tar.getmembers():
|
||||
# A hostile bundle must not escape the workdir.
|
||||
if m.name.startswith(("/", "..")) or ".." in m.name.split("/"):
|
||||
continue
|
||||
tar.extract(m, WORKDIR)
|
||||
with open(STAMP, "w") as f:
|
||||
f.write(VERSION)
|
||||
|
||||
|
||||
def ensure_bundle() -> bool:
|
||||
try:
|
||||
with open(STAMP) as f:
|
||||
if f.read().strip() == VERSION and os.path.isdir(os.path.join(WORKDIR, "backend")):
|
||||
return True
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
fetch_and_unpack()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[runner] bundle fetch failed: {type(e).__name__}: {e}", flush=True)
|
||||
return False
|
||||
|
||||
|
||||
def install_requirements() -> None:
|
||||
req = os.path.join(WORKDIR, "backend", "requirements.txt")
|
||||
if not os.path.isfile(req):
|
||||
return
|
||||
# Best-effort: the base image preinstalls the template's deps; this covers apps that added more.
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", "--no-cache-dir", "-q", "-r", req],
|
||||
timeout=180, check=False,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not SLUG or not ensure_bundle():
|
||||
# Serve an honest 503 shell instead of crash-looping the machine.
|
||||
os.execvp(sys.executable, [sys.executable, "-m", "uvicorn", "fallback:app", "--host", "0.0.0.0", "--port", "8080"])
|
||||
install_requirements()
|
||||
os.chdir(WORKDIR)
|
||||
sys.path.insert(0, WORKDIR)
|
||||
spec = {}
|
||||
try:
|
||||
with open(os.path.join(WORKDIR, "runspec.json")) as f:
|
||||
spec = json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
entry = "serve:app" if spec.get("has_backend") else "fallback:app"
|
||||
os.environ["PYTHONPATH"] = WORKDIR + ":" + os.environ.get("PYTHONPATH", "") + ":/srv/runner"
|
||||
os.execvp(sys.executable, [sys.executable, "-m", "uvicorn", entry, "--host", "0.0.0.0", "--port", "8080", "--app-dir", "/srv/runner"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user