cli: graceful fallback when --write-env can't write the host-mounted .env
Some checks failed
CI / ruff (push) Has been cancelled
CI / mypy --strict (push) Has been cancelled
CI / pytest (push) Has been cancelled
CI / bandit (push) Has been cancelled
CI / pip-audit (push) Has been cancelled

The CLI runs inside the gateway container as the non-root `gateway` user
(uid 10001). The .env file is typically host-mounted at /app/.env, owned
by the host user — so the container process can't write the
.env.tmp file `update_env_file()` creates for the atomic rename. That
surfaced as a raw PermissionError traceback from
`docker exec neuronetz-gateway neuronetz-gateway remove-backend …`.

Now both add-backend and remove-backend catch PermissionError / OSError
on the write, print a tidy red message, and tell the user the two ways
forward: re-run with `docker exec -u root …` (in-container root keeps
the same FS, just bypasses ownership), or paste the printed
OLLAMA_BACKENDS line manually. Exit 1 either way so scripts notice.
This commit is contained in:
Stephan Berbig
2026-05-27 23:07:48 +02:00
parent c9e11c3486
commit 64f1ebc484

View File

@@ -448,6 +448,27 @@ def probe_ollama(
typer.secho("upstream reachable and authenticated.", fg=typer.colors.GREEN, bold=True) typer.secho("upstream reachable and authenticated.", fg=typer.colors.GREEN, bold=True)
def _emit_paste_fallback(path: Path, line: str, exc: BaseException) -> None:
"""Print a clean fallback message when --write-env can't write the file.
The bootstrap CLI usually runs inside the gateway container (uid 10001),
but the host-mounted .env is typically owned by the host user — so the
in-container process can't write it. Instead of crashing with a traceback,
print the line the user can paste, and tell them how to retry with the
right privileges.
"""
typer.secho(f"✗ could not write {path}: {type(exc).__name__}", fg=typer.colors.RED)
typer.echo(f" ({exc})")
typer.echo("")
typer.echo("Two ways forward:")
typer.echo(" 1. Re-run the same command with `docker exec -u root …` so the in-container")
typer.echo(" process can write to the host-mounted .env.")
typer.echo(" 2. Paste this into your .env manually, then `docker compose up -d gateway`:")
typer.echo("")
typer.echo(f" OLLAMA_BACKENDS={line}")
typer.echo("")
@app.command("list-backends") @app.command("list-backends")
def list_backends() -> None: def list_backends() -> None:
"""Show the configured Ollama backends (tokens redacted). """Show the configured Ollama backends (tokens redacted).
@@ -588,7 +609,11 @@ def add_backend(
line = serialize(updated) line = serialize(updated)
if write_env is not None: if write_env is not None:
path = Path(write_env) path = Path(write_env)
update_env_file(path, line) try:
update_env_file(path, line)
except (PermissionError, OSError) as exc:
_emit_paste_fallback(path, line, exc)
raise typer.Exit(code=1) from exc
typer.echo(f"✓ updated {path}") typer.echo(f"✓ updated {path}")
typer.echo(" recreate the gateway: docker compose up -d gateway") typer.echo(" recreate the gateway: docker compose up -d gateway")
else: else:
@@ -643,7 +668,12 @@ def remove_backend(
return return
if write_env is not None: if write_env is not None:
update_env_file(Path(write_env), line) path = Path(write_env)
try:
update_env_file(path, line)
except (PermissionError, OSError) as exc:
_emit_paste_fallback(path, line, exc)
raise typer.Exit(code=1) from exc
typer.echo(f"✓ updated {write_env}") typer.echo(f"✓ updated {write_env}")
typer.echo(" recreate the gateway: docker compose up -d gateway") typer.echo(" recreate the gateway: docker compose up -d gateway")
else: else: