Add the fluksio CLI: serve, enroll, worker
`pip install fluksio && fluksio serve` on a machine with no Docker, no database and no configuration — which is the case this is for: a node on a cluster where ports cannot be opened. It makes its data directory, its key and an admin account, prints the password once, and serves. Pairing is `fluksio enroll <code> --portal …`, doing what the Settings screen does through the same function, before the engine starts and without one running — a machine nobody can route to has no browser pointed at it either. The portal serves the dashboard, so nothing is served here. Two things had to give way. `fastapi[standard]` pulls a cloud CLI that wants sentry-sdk 2.x while we pinned below it — no pip resolution existed, so the pin is lifted, which the comment beside it had been waiting for and which also lets the Python cap go. And `uv` is now a dependency rather than something to find on PATH: the Modules screen is how a data scientist installs torch, and it was quietly falling back to the engine's own interpreter. The CLI imports nothing from the engine before it has set DATA_DIR — the settings are built on the first import of core.config, and reaching it early put the database in the working directory. There is a test for that now, because the failure is silent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
"""First run: an account to sign in with.
|
||||
|
||||
An installation started from the command line is given no environment, so the
|
||||
superuser a deployment sets in `.env` has to come from somewhere. The session
|
||||
key is the CLI's own business — it has to be set before this module can be
|
||||
imported at all.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from fluksio import crud
|
||||
from fluksio.models import User, UserCreate
|
||||
|
||||
#: A generated admin's address. `example.com` is reserved for exactly this
|
||||
#: (RFC 2606) — `@localhost` and `.local` are too, and `EmailStr` refuses those.
|
||||
#: It is a name to sign in with, not somewhere mail is sent.
|
||||
DEFAULT_ADMIN = "admin@example.com"
|
||||
|
||||
|
||||
def ensure_superuser(
|
||||
session: Session, *, email: str | None = None, password: str | None = None
|
||||
) -> tuple[User, str | None]:
|
||||
"""The account to sign in with, made on first run.
|
||||
|
||||
Returns the user and, when it was just created, the password in clear —
|
||||
the caller prints it once. An installation that already has a superuser is
|
||||
left alone: this is a first run, not a password reset.
|
||||
"""
|
||||
existing = session.exec(
|
||||
select(User).where(User.is_superuser == True) # noqa: E712
|
||||
).first()
|
||||
if existing is not None:
|
||||
return existing, None
|
||||
|
||||
generated = password or secrets.token_urlsafe(12)
|
||||
user = crud.create_user(
|
||||
session=session,
|
||||
user_create=UserCreate(
|
||||
email=email or DEFAULT_ADMIN, password=generated, is_superuser=True
|
||||
),
|
||||
)
|
||||
return user, generated
|
||||
|
||||
|
||||
def pick_superuser(session: Session, email: str | None = None) -> User:
|
||||
"""The account an enrolment acts as; a portal session arrives as this one."""
|
||||
if email:
|
||||
user = session.exec(select(User).where(User.email == email)).first()
|
||||
if user is None:
|
||||
raise LookupError(f"No account here with the address {email}")
|
||||
return user
|
||||
users = session.exec(
|
||||
select(User).where(User.is_superuser == True) # noqa: E712
|
||||
).all()
|
||||
if not users:
|
||||
raise LookupError("This installation has no superuser to enrol as")
|
||||
if len(users) > 1:
|
||||
addresses = ", ".join(sorted(u.email for u in users))
|
||||
raise LookupError(f"Several superusers here — name one with --as: {addresses}")
|
||||
return users[0]
|
||||
Reference in New Issue
Block a user