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,36 @@
|
||||
"""The command line, and the one ordering it depends on."""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from fluksio.cli import load_or_create_secret_key
|
||||
|
||||
|
||||
def test_importing_the_cli_does_not_build_the_settings() -> None:
|
||||
"""`_configure_environment` has to run before anything reads a setting.
|
||||
|
||||
The settings are built on the first import of `fluksio.core.config`, and
|
||||
every engine module reaches it within an import or two. If importing the
|
||||
CLI pulled it in, `DATA_DIR` would be fixed at whatever directory the
|
||||
command was run from — which is how the database ends up in the cwd.
|
||||
"""
|
||||
leaked = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-c",
|
||||
"import fluksio.cli, sys; print('fluksio.core.config' in sys.modules)",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
assert leaked.stdout.strip() == "False", leaked.stdout
|
||||
|
||||
|
||||
def test_the_secret_key_is_kept_rather_than_regenerated(tmp_path: Path) -> None:
|
||||
"""A new key each start would sign out every session and orphan secrets.enc."""
|
||||
path = tmp_path / "secret_key"
|
||||
first = load_or_create_secret_key(path)
|
||||
assert load_or_create_secret_key(path) == first
|
||||
assert path.stat().st_mode & 0o777 == 0o600
|
||||
@@ -10,7 +10,7 @@ nothing, which is what makes deleting that local account a revocation.
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import jwt
|
||||
import pytest
|
||||
@@ -206,9 +206,7 @@ def test_adding_a_remote_user_maps_and_revokes(
|
||||
return_value={"user_id": "portal-user-9", "email": "remote@example.com"}
|
||||
),
|
||||
)
|
||||
with patch(
|
||||
"fluksio.api.routes.cloud.httpx.post", return_value=portal_reply
|
||||
) as post:
|
||||
with patch("fluksio.cloud.enroll.httpx.post", return_value=portal_reply) as post:
|
||||
added = client.post(
|
||||
f"{settings.API_V1_STR}/cloud/users",
|
||||
headers=superuser_token_headers,
|
||||
@@ -226,7 +224,7 @@ def test_adding_a_remote_user_maps_and_revokes(
|
||||
resolved = user_from_token(db, theirs)
|
||||
assert resolved is not None and resolved.email == "remote@example.com"
|
||||
|
||||
with patch("fluksio.api.routes.cloud.httpx.post", return_value=portal_reply):
|
||||
with patch("fluksio.cloud.enroll.httpx.post", return_value=portal_reply):
|
||||
again = client.post(
|
||||
f"{settings.API_V1_STR}/cloud/users",
|
||||
headers=superuser_token_headers,
|
||||
@@ -281,10 +279,7 @@ def test_enrolling_against_a_portal_without_an_owner_is_refused(
|
||||
),
|
||||
)
|
||||
try:
|
||||
with patch("fluksio.api.routes.cloud.httpx.AsyncClient") as client_cls:
|
||||
client_cls.return_value.__aenter__.return_value.post = AsyncMock(
|
||||
return_value=reply
|
||||
)
|
||||
with patch("fluksio.cloud.enroll.httpx.post", return_value=reply):
|
||||
response = client.post(
|
||||
f"{settings.API_V1_STR}/cloud/enroll",
|
||||
headers=superuser_token_headers,
|
||||
|
||||
Reference in New Issue
Block a user