Start without git, and say what that costs
A `pip install` on a locked-down host — the case the CLI exists for — may have no git, and the store shelled out to it while building the flow repository, so the engine refused to start at all. The store is files; git is their history. Missing it is now one warning and no commits rather than a stack trace, which is the difference between a machine that runs your experiments and one that does not. Found by installing the wheels into a bare python:3.12-slim and pairing it with the portal: `fluksio enroll` took the code, `fluksio serve` dialled out, and the hub was proxying requests through the tunnel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,8 @@ fluksio enroll <code> --portal https://hub.fluksio.com # watch it from the por
|
|||||||
|
|
||||||
Nothing else has to be running. `--data-dir` puts the installation somewhere else —
|
Nothing else has to be running. `--data-dir` puts the installation somewhere else —
|
||||||
worth it on a cluster, where `$HOME` is often a network filesystem SQLite cannot use.
|
worth it on a cluster, where `$HOME` is often a network filesystem SQLite cannot use.
|
||||||
|
`git` is not required but is worth having: flows are files either way, and it is what
|
||||||
|
turns each save into a commit.
|
||||||
The dashboard is served by the portal, so a machine with no inbound route is reached
|
The dashboard is served by the portal, so a machine with no inbound route is reached
|
||||||
without opening a port: it dials out.
|
without opening a port: it dials out.
|
||||||
|
|
||||||
|
|||||||
+10
-15
@@ -124,14 +124,17 @@ def _enroll(portal: str, code: str, as_email: str | None) -> int:
|
|||||||
from fluksio.core.db import engine
|
from fluksio.core.db import engine
|
||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
_, generated = ensure_superuser(session, email=as_email)
|
admin, generated = ensure_superuser(session, email=as_email)
|
||||||
if generated:
|
if generated:
|
||||||
_print_new_admin(session, generated)
|
_print_new_admin(admin.email, generated)
|
||||||
try:
|
try:
|
||||||
user = pick_superuser(session, as_email)
|
user = pick_superuser(session, as_email)
|
||||||
except LookupError as exc:
|
except LookupError as exc:
|
||||||
print(f"error: {exc}", file=sys.stderr, flush=True)
|
print(f"error: {exc}", file=sys.stderr, flush=True)
|
||||||
return 1
|
return 1
|
||||||
|
# Read before the session closes: the instance is detached after it,
|
||||||
|
# and touching an attribute then goes back to a database that is gone.
|
||||||
|
email = user.email
|
||||||
try:
|
try:
|
||||||
config = enroll_mod.enroll(session, user, portal, code)
|
config = enroll_mod.enroll(session, user, portal, code)
|
||||||
except enroll_mod.AlreadyEnrolled as exc:
|
except enroll_mod.AlreadyEnrolled as exc:
|
||||||
@@ -146,22 +149,14 @@ def _enroll(portal: str, code: str, as_email: str | None) -> int:
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
_say(
|
_say(
|
||||||
f"Connected to {config.portal_url} as {user.email} "
|
f"Connected to {config.portal_url} as {email} "
|
||||||
f"(installation {config.installation_id})."
|
f"(installation {config.installation_id})."
|
||||||
)
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def _print_new_admin(session: object, password: str) -> None:
|
def _print_new_admin(email: str, password: str) -> None:
|
||||||
from sqlmodel import Session, select
|
_say(f"Created the admin account {email}")
|
||||||
|
|
||||||
from fluksio.models import User
|
|
||||||
|
|
||||||
assert isinstance(session, Session)
|
|
||||||
user = session.exec(
|
|
||||||
select(User).where(User.is_superuser == True) # noqa: E712
|
|
||||||
).first()
|
|
||||||
_say(f"Created the admin account {user.email if user else ''}")
|
|
||||||
_say(f" password: {password}")
|
_say(f" password: {password}")
|
||||||
_say(" Shown once. Change it from the dashboard.")
|
_say(" Shown once. Change it from the dashboard.")
|
||||||
|
|
||||||
@@ -177,11 +172,11 @@ def cmd_serve(args: argparse.Namespace) -> int:
|
|||||||
from fluksio.core.db import engine
|
from fluksio.core.db import engine
|
||||||
|
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
_, generated = ensure_superuser(
|
admin, generated = ensure_superuser(
|
||||||
session, email=args.admin_email, password=args.admin_password
|
session, email=args.admin_email, password=args.admin_password
|
||||||
)
|
)
|
||||||
if generated:
|
if generated:
|
||||||
_print_new_admin(session, generated)
|
_print_new_admin(admin.email, generated)
|
||||||
|
|
||||||
if args.enroll:
|
if args.enroll:
|
||||||
if not args.portal:
|
if not args.portal:
|
||||||
|
|||||||
@@ -94,6 +94,9 @@ def _same_content(left: FlowDef, right: FlowDef) -> bool:
|
|||||||
class FlowStore:
|
class FlowStore:
|
||||||
"""Reads and writes flows, committing every change."""
|
"""Reads and writes flows, committing every change."""
|
||||||
|
|
||||||
|
#: One warning per process when there is no git to commit with.
|
||||||
|
_warned_no_git = False
|
||||||
|
|
||||||
def __init__(self, root: Path) -> None:
|
def __init__(self, root: Path) -> None:
|
||||||
self.root = root
|
self.root = root
|
||||||
self.root.mkdir(parents=True, exist_ok=True)
|
self.root.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -109,12 +112,27 @@ class FlowStore:
|
|||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
def _git(self, *args: str) -> subprocess.CompletedProcess[str]:
|
def _git(self, *args: str) -> subprocess.CompletedProcess[str]:
|
||||||
return subprocess.run(
|
try:
|
||||||
["git", "-C", str(self.root), *args],
|
return subprocess.run(
|
||||||
capture_output=True,
|
["git", "-C", str(self.root), *args],
|
||||||
text=True,
|
capture_output=True,
|
||||||
check=False,
|
text=True,
|
||||||
)
|
check=False,
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
# No git on this machine — a `pip install` on a locked-down host is
|
||||||
|
# where this happens. The flows are files either way, which is what
|
||||||
|
# the store is; what is lost is their history, so say it once and
|
||||||
|
# carry on rather than refusing to start.
|
||||||
|
if not FlowStore._warned_no_git:
|
||||||
|
FlowStore._warned_no_git = True
|
||||||
|
logger.warning(
|
||||||
|
"git is not installed, so flow changes are not versioned. "
|
||||||
|
"Install it to get a commit per save."
|
||||||
|
)
|
||||||
|
return subprocess.CompletedProcess(
|
||||||
|
args=list(args), returncode=1, stdout="", stderr="git is not installed"
|
||||||
|
)
|
||||||
|
|
||||||
def _commit(self, message: str, allow_empty: bool = False) -> None:
|
def _commit(self, message: str, allow_empty: bool = False) -> None:
|
||||||
self._git("add", "-A")
|
self._git("add", "-A")
|
||||||
@@ -129,8 +147,12 @@ class FlowStore:
|
|||||||
"-m",
|
"-m",
|
||||||
message,
|
message,
|
||||||
)
|
)
|
||||||
if result.returncode != 0 and "nothing to commit" not in result.stdout:
|
if result.returncode == 0 or "nothing to commit" in result.stdout:
|
||||||
logger.warning("Could not commit flow change: %s", result.stdout.strip())
|
return
|
||||||
|
if result.stderr == "git is not installed":
|
||||||
|
# Already said once, in `_git`. Repeating it per save is noise.
|
||||||
|
return
|
||||||
|
logger.warning("Could not commit flow change: %s", result.stdout.strip())
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# Paths
|
# Paths
|
||||||
|
|||||||
@@ -34,3 +34,29 @@ def test_the_secret_key_is_kept_rather_than_regenerated(tmp_path: Path) -> None:
|
|||||||
first = load_or_create_secret_key(path)
|
first = load_or_create_secret_key(path)
|
||||||
assert load_or_create_secret_key(path) == first
|
assert load_or_create_secret_key(path) == first
|
||||||
assert path.stat().st_mode & 0o777 == 0o600
|
assert path.stat().st_mode & 0o777 == 0o600
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_store_works_without_git(tmp_path: Path, monkeypatch) -> None:
|
||||||
|
"""A pip install on a locked-down host may have no git.
|
||||||
|
|
||||||
|
Flows are files, and that is what the store is for; the history is the part
|
||||||
|
that needs git. Losing it must not be a refusal to start.
|
||||||
|
"""
|
||||||
|
import subprocess as sp
|
||||||
|
|
||||||
|
from fluksio.flow.store import FlowStore
|
||||||
|
|
||||||
|
real_run = sp.run
|
||||||
|
|
||||||
|
def no_git(cmd, *args, **kwargs):
|
||||||
|
if cmd and cmd[0] == "git":
|
||||||
|
raise FileNotFoundError(2, "No such file or directory", "git")
|
||||||
|
return real_run(cmd, *args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr(sp, "run", no_git)
|
||||||
|
monkeypatch.setattr(FlowStore, "_warned_no_git", False)
|
||||||
|
|
||||||
|
store = FlowStore(tmp_path / "flows")
|
||||||
|
assert store.head() == ""
|
||||||
|
store.write_requirements("numpy\n")
|
||||||
|
assert store.read_requirements() == "numpy\n"
|
||||||
|
|||||||
Reference in New Issue
Block a user