Skip to content

Commit 5e4c9cb

Browse files
committed
Merge branch 'add/juicefs-mounting' of https://github.com/transformerlab/transformerlab-app into add/juicefs-mounting
2 parents b5e1209 + 9d8a3ed commit 5e4c9cb

5 files changed

Lines changed: 140 additions & 10 deletions

File tree

api/api.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ async def lifespan(app: FastAPI):
9797

9898
# Do the following at API Startup:
9999
print_launch_message()
100+
_print_storage_banner()
100101
# Initialize directories early
101102
from transformerlab.shared import dirs as shared_dirs
102103

@@ -362,6 +363,13 @@ async def healthz():
362363
"metadata": {
363364
"email_method": email_method,
364365
},
366+
# Only the provider is exposed here — /healthz is unauthenticated, so
367+
# the bucket/path URI must NOT leak. The full URI is logged at startup
368+
# (operator-visible) and can be added to authenticated endpoints if
369+
# the CLI/UI ever needs it.
370+
"storage": {
371+
"provider": _effective_storage_provider(),
372+
},
365373
}
366374

367375

@@ -410,6 +418,46 @@ def print_launch_message():
410418
print("https://lab.cloud\nhttps://github.com/transformerlab/transformerlab-app\n")
411419

412420

421+
def _effective_storage_provider() -> str:
422+
"""Return the storage backend that's actually in use.
423+
424+
`TFL_STORAGE_PROVIDER` alone isn't authoritative: when it's set to
425+
aws/gcp/azure but `TFL_REMOTE_STORAGE_ENABLED` isn't `true`, the SDK
426+
silently falls back to the local filesystem at `~/.transformerlab`.
427+
Reporting the configured provider in that case is misleading, so callers
428+
should use this helper instead.
429+
"""
430+
from lab.storage import STORAGE_PROVIDER
431+
432+
if STORAGE_PROVIDER == "localfs":
433+
return "localfs"
434+
remote_enabled = os.getenv("TFL_REMOTE_STORAGE_ENABLED", "false").lower() == "true"
435+
return STORAGE_PROVIDER if remote_enabled else "localfs"
436+
437+
438+
def _print_storage_banner():
439+
"""Print the active storage backend so it's obvious in API logs."""
440+
from lab.storage import STORAGE_PROVIDER
441+
442+
effective = _effective_storage_provider()
443+
uri = os.getenv("TFL_STORAGE_URI")
444+
if effective == "localfs":
445+
# Either configured as localfs, or remote was requested but not enabled —
446+
# in both cases the SDK uses ~/.transformerlab/workspace.
447+
from lab.dirs import HOME_DIR
448+
449+
if not uri:
450+
uri = os.path.join(HOME_DIR, "orgs", "<org_id>", "workspace")
451+
if effective != STORAGE_PROVIDER:
452+
# Surface the misconfiguration so operators don't think remote is live.
453+
print(
454+
f"✅ Storage: {effective} ({uri}) "
455+
f"[TFL_STORAGE_PROVIDER={STORAGE_PROVIDER} ignored: TFL_REMOTE_STORAGE_ENABLED is not set]"
456+
)
457+
return
458+
print(f"✅ Storage: {effective} ({uri or 'unset'})")
459+
460+
413461
def run():
414462
args = parse_args()
415463

api/test/api/test_server_info.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import pytest
2+
3+
14
def test_is_wsl_false(monkeypatch):
25
# Simulate subprocess.CalledProcessError
36
import subprocess
@@ -46,3 +49,52 @@ def test_healthz_localfs_mode(client, monkeypatch, tmp_path):
4649
data = response.json()
4750
assert data["message"] == "OK"
4851
assert data["mode"] == "multiuser"
52+
53+
54+
def test_healthz_includes_storage_provider_only(client, monkeypatch, tmp_path):
55+
"""healthz exposes the storage provider but NOT the URI.
56+
57+
/healthz is unauthenticated, so leaking the bucket/path would be infra
58+
reconnaissance fuel for any caller. The full URI is operator-visible
59+
in the startup banner only.
60+
"""
61+
monkeypatch.setenv("TFL_STORAGE_URI", str(tmp_path / "storage_root"))
62+
63+
response = client.get("/healthz")
64+
assert response.status_code == 200
65+
data = response.json()
66+
assert "storage" in data
67+
assert "provider" in data["storage"]
68+
assert "uri" not in data["storage"], "URI must not be exposed via /healthz (unauthenticated endpoint)"
69+
70+
71+
def test_healthz_storage_reports_localfs_when_remote_not_enabled(client, monkeypatch):
72+
"""When TFL_STORAGE_PROVIDER is cloud but TFL_REMOTE_STORAGE_ENABLED is unset,
73+
the SDK falls back to the local filesystem — /healthz must reflect that
74+
rather than reporting the configured-but-inactive cloud backend.
75+
"""
76+
# STORAGE_PROVIDER is captured at lab.storage import time, so setenv on
77+
# TFL_STORAGE_PROVIDER would be a no-op here — patch the module attribute.
78+
import lab.storage
79+
80+
monkeypatch.setattr(lab.storage, "STORAGE_PROVIDER", "aws")
81+
monkeypatch.delenv("TFL_REMOTE_STORAGE_ENABLED", raising=False)
82+
83+
response = client.get("/healthz")
84+
assert response.status_code == 200
85+
data = response.json()
86+
assert data["storage"]["provider"] == "localfs"
87+
88+
89+
@pytest.mark.parametrize("provider", ["aws", "gcp", "azure"])
90+
def test_healthz_storage_reports_cloud_when_remote_enabled(client, monkeypatch, provider):
91+
"""When TFL_REMOTE_STORAGE_ENABLED=true, /healthz reports the configured cloud provider."""
92+
import lab.storage
93+
94+
monkeypatch.setattr(lab.storage, "STORAGE_PROVIDER", provider)
95+
monkeypatch.setenv("TFL_REMOTE_STORAGE_ENABLED", "true")
96+
97+
response = client.get("/healthz")
98+
assert response.status_code == 200
99+
data = response.json()
100+
assert data["storage"]["provider"] == provider

cli/src/transformerlab_cli/commands/job.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -925,9 +925,11 @@ def command_job_discard(
925925
"""Toggle score.discard on a job."""
926926
current_experiment = require_current_experiment()
927927
discard_value = not undo
928+
payload = json.dumps({"updates": {"discard": discard_value}}).encode("utf-8")
928929
response = api.put(
929930
f"/experiment/{current_experiment}/jobs/{job_id}/job_data",
930-
json={"updates": {"discard": discard_value}},
931+
content=payload,
932+
headers={"Content-Type": "application/json"},
931933
)
932934
if response.status_code == 200:
933935
if cli_state.output_format == "json":

cli/src/transformerlab_cli/util/config.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,32 @@
2020
# to avoid repeated file reads
2121
cached_config = None
2222

23+
# Cached /healthz response so the header doesn't refetch per-process.
24+
# Sentinel: None = not yet attempted; {} = fetch failed (don't retry).
25+
_cached_healthz: dict[str, Any] | None = None
26+
27+
28+
def _get_storage_label() -> str:
29+
"""Return a short storage backend label for the header, e.g. 'aws' or '?'."""
30+
global _cached_healthz
31+
if _cached_healthz is None:
32+
_cached_healthz = {}
33+
try:
34+
# Imported lazily so config import doesn't pull httpx at startup
35+
from transformerlab_cli.util.api import get
36+
37+
response = get("/healthz", timeout=2.0)
38+
if response.status_code == 200:
39+
_cached_healthz = response.json() or {}
40+
except Exception:
41+
_cached_healthz = {}
42+
43+
storage = _cached_healthz.get("storage") if isinstance(_cached_healthz, dict) else None
44+
if not isinstance(storage, dict):
45+
return "?"
46+
provider = storage.get("provider")
47+
return str(provider) if provider else "?"
48+
2349

2450
def load_config() -> dict[str, Any]:
2551
"""Load config from file, return empty dict if not found.
@@ -222,15 +248,17 @@ def check_configs(output_format: str = "pretty") -> None:
222248
team_name = config.get("team_id", "N/A")
223249
server = config.get("server", "N/A")
224250
experiment = config.get("current_experiment", "N/A")
251+
storage = _get_storage_label()
225252

226253
table = Table(show_header=True, header_style="header", box=None, title_justify="left")
227254

228255
table.add_column("User Email", style="value")
229256
table.add_column("Team ID", style="value")
230257
table.add_column("Server", style="value")
231258
table.add_column("Experiment", style="value")
259+
table.add_column("Storage", style="value")
232260

233-
table.add_row(user_email, team_name, server, experiment)
261+
table.add_row(user_email, team_name, server, experiment, storage)
234262
console.rule()
235263
one_liner_logo(console)
236264
console.print(table)

cli/tests/commands/test_job.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -612,10 +612,10 @@ def test_job_discard_sets_discard_true(_mock_require, mock_put):
612612

613613
result = runner.invoke(app, ["job", "discard", "42"])
614614
assert result.exit_code == 0
615-
mock_put.assert_called_once_with(
616-
"/experiment/exp1/jobs/42/job_data",
617-
json={"updates": {"discard": True}},
618-
)
615+
mock_put.assert_called_once()
616+
assert mock_put.call_args.args[0] == "/experiment/exp1/jobs/42/job_data"
617+
assert mock_put.call_args.kwargs["headers"] == {"Content-Type": "application/json"}
618+
assert json.loads(mock_put.call_args.kwargs["content"].decode("utf-8")) == {"updates": {"discard": True}}
619619

620620

621621
@patch("transformerlab_cli.commands.job.api.put")
@@ -627,10 +627,10 @@ def test_job_discard_undo_sets_discard_false(_mock_require, mock_put):
627627

628628
result = runner.invoke(app, ["job", "discard", "42", "--undo"])
629629
assert result.exit_code == 0
630-
mock_put.assert_called_once_with(
631-
"/experiment/exp1/jobs/42/job_data",
632-
json={"updates": {"discard": False}},
633-
)
630+
mock_put.assert_called_once()
631+
assert mock_put.call_args.args[0] == "/experiment/exp1/jobs/42/job_data"
632+
assert mock_put.call_args.kwargs["headers"] == {"Content-Type": "application/json"}
633+
assert json.loads(mock_put.call_args.kwargs["content"].decode("utf-8")) == {"updates": {"discard": False}}
634634

635635

636636
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)