One installation per project, and no login to reach it
Two things a local install should not have asked for. `fluksio serve` now signs you in. Logging in to your own machine was a formality — the password was printed by the same process that would have checked it, and the database it authenticates against sits in the directory the token goes into — so `serve` mints the token itself and says where it put it. `fluksio login` is left for an engine somewhere else. And an installation is `.fluksio` beside the code, found the way `.git` is, rather than one `~/.fluksio` for the machine. A repository with its own venv was already getting its own engine; it now gets its own flows, run history and token too, instead of three repositories sharing one database and fighting over one port. `--global` asks for the shared one, `--data-dir` still names any directory, and when both exist the banner says which you are looking at and how to reach the other. The directory ignores itself from within — a `.gitignore` of `*`, the way uv writes one into `.venv` — because it holds a credential and a database, and neither belongs in anybody's history. The token is written mode 600. A login an older version wrote to ~/.config/fluksio is still read, so nothing that worked stops working. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ue1tkFWB1bcGy3aWhCKpU
This commit is contained in:
+76
-7
@@ -20,6 +20,7 @@ import os
|
||||
import secrets
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import fluksio
|
||||
|
||||
@@ -41,12 +42,37 @@ def _say(message: str = "") -> None:
|
||||
print(message, flush=True)
|
||||
|
||||
|
||||
def _data_dir(raw: str | None) -> Path:
|
||||
path = Path(raw).expanduser() if raw else DEFAULT_HOME.expanduser()
|
||||
def _data_dir(raw: str | None, shared: bool = False) -> Path:
|
||||
"""Which installation this command is for.
|
||||
|
||||
A repository with its own venv wants its own engine too — its own flows,
|
||||
its own run history, its own token — so the default is a `.fluksio` beside
|
||||
the code, found the way `.git` is. `--global` asks for the shared one
|
||||
instead, and `--data-dir` names any directory outright.
|
||||
"""
|
||||
from fluksio.sdk.client import find_data_dir, ignore_self
|
||||
|
||||
if raw:
|
||||
path = Path(raw).expanduser()
|
||||
elif shared:
|
||||
path = DEFAULT_HOME.expanduser()
|
||||
else:
|
||||
found = find_data_dir()
|
||||
path = found if found is not None else Path.cwd() / ".fluksio"
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
ignore_self(path)
|
||||
return path.resolve()
|
||||
|
||||
|
||||
def _mention_other_installation(data_dir: Path) -> None:
|
||||
"""Say when there is a second engine, so neither goes looking lost."""
|
||||
shared = DEFAULT_HOME.expanduser().resolve()
|
||||
if data_dir == shared or not (shared / "fluksio.db").exists():
|
||||
return
|
||||
_say(f" Note {shared} holds another installation; this one is separate.")
|
||||
_say(" `fluksio serve --global` runs that one instead.")
|
||||
|
||||
|
||||
def _warn_if_networked(path: Path) -> None:
|
||||
"""A cluster's $HOME is often NFS, and SQLite's WAL does not work there."""
|
||||
try:
|
||||
@@ -161,8 +187,35 @@ def _print_new_admin(email: str, password: str) -> None:
|
||||
_say(" Shown once. Change it from the dashboard.")
|
||||
|
||||
|
||||
#: How long the token `serve` writes for its own machine is good for. Long,
|
||||
#: because the thing it saves you from is logging in again, and it is rewritten
|
||||
#: on every start anyway — an engine left running for a year is the only case
|
||||
#: this has to cover.
|
||||
LOCAL_TOKEN_DAYS = 365
|
||||
|
||||
|
||||
def _sign_in(admin_id: Any, url: str, data_dir: Path) -> Path:
|
||||
"""Write the credential for the engine this command is about to start.
|
||||
|
||||
Logging in to your own machine is a formality: the password was printed by
|
||||
this same process, and the database it authenticates against is in the
|
||||
directory the token goes into. So it costs nothing to be honest about it
|
||||
and hand the client a token, rather than asking somebody to type back
|
||||
something we just told them.
|
||||
"""
|
||||
from datetime import timedelta
|
||||
|
||||
from fluksio.core import security
|
||||
from fluksio.sdk.client import write_config
|
||||
|
||||
token = security.create_access_token(
|
||||
admin_id, expires_delta=timedelta(days=LOCAL_TOKEN_DAYS)
|
||||
)
|
||||
return write_config(url, token, data_dir)
|
||||
|
||||
|
||||
def cmd_serve(args: argparse.Namespace) -> int:
|
||||
data_dir = _data_dir(args.data_dir)
|
||||
data_dir = _data_dir(args.data_dir, args.shared)
|
||||
_prepare(data_dir)
|
||||
|
||||
from sqlmodel import Session
|
||||
@@ -175,8 +228,9 @@ def cmd_serve(args: argparse.Namespace) -> int:
|
||||
admin, generated = ensure_superuser(
|
||||
session, email=args.admin_email, password=args.admin_password
|
||||
)
|
||||
admin_id, admin_email = admin.id, admin.email
|
||||
if generated:
|
||||
_print_new_admin(admin.email, generated)
|
||||
_print_new_admin(admin_email, generated)
|
||||
|
||||
if args.enroll:
|
||||
if not args.portal:
|
||||
@@ -194,9 +248,15 @@ def cmd_serve(args: argparse.Namespace) -> int:
|
||||
from fluksio.flow import modules
|
||||
from fluksio.main import app
|
||||
|
||||
# The client talks to this engine, and 0.0.0.0 is not an address to talk
|
||||
# to — it is a statement about which interfaces to listen on.
|
||||
reachable = "127.0.0.1" if args.host in ("0.0.0.0", "::", "") else args.host
|
||||
url = f"http://{reachable}:{args.port}"
|
||||
token_path = _sign_in(admin_id, url, data_dir)
|
||||
|
||||
config = cloud_config.load()
|
||||
_say(f"Fluksio {fluksio.__version__} — data in {data_dir}")
|
||||
_say(f" API http://{args.host}:{args.port}{'/api/v1'}")
|
||||
_say(f" API {url}/api/v1")
|
||||
# Which environment node code imports from is the thing a data scientist
|
||||
# most needs to know at this moment, and the answer differs depending on
|
||||
# how Fluksio was installed. Saying it costs one line.
|
||||
@@ -215,6 +275,9 @@ def cmd_serve(args: argparse.Namespace) -> int:
|
||||
else:
|
||||
_say(" No portal. Pair this installation with:")
|
||||
_say(" fluksio enroll <code> --portal https://hub.example.com")
|
||||
_say(f" Signed in as {admin_email}")
|
||||
_say(f" token in {token_path}")
|
||||
_mention_other_installation(data_dir)
|
||||
|
||||
# One process: it holds the flow engine, and a second worker would be a
|
||||
# second engine — duplicated subscriptions, cron ticks and webhooks.
|
||||
@@ -223,7 +286,7 @@ def cmd_serve(args: argparse.Namespace) -> int:
|
||||
|
||||
|
||||
def cmd_enroll(args: argparse.Namespace) -> int:
|
||||
data_dir = _data_dir(args.data_dir)
|
||||
data_dir = _data_dir(args.data_dir, args.shared)
|
||||
_prepare(data_dir)
|
||||
result = _enroll(args.portal, args.code, args.as_email)
|
||||
if result == 0:
|
||||
@@ -248,7 +311,13 @@ def _parser() -> argparse.ArgumentParser:
|
||||
sub.add_argument(
|
||||
"--data-dir",
|
||||
default=os.environ.get("FLUKSIO_HOME"),
|
||||
help=f"where this installation keeps everything (default {DEFAULT_HOME})",
|
||||
help="where this installation keeps everything (default: ./.fluksio)",
|
||||
)
|
||||
sub.add_argument(
|
||||
"--global",
|
||||
dest="shared",
|
||||
action="store_true",
|
||||
help=f"use the shared installation at {DEFAULT_HOME} instead",
|
||||
)
|
||||
|
||||
serve = subparsers.add_parser("serve", help="run the engine")
|
||||
|
||||
Reference in New Issue
Block a user