Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 11 additions & 19 deletions osa/cli/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ def _store_dev_credentials(project_dir: Path) -> None:
ORCID_SANDBOX=true
ORCID_ADMINS=[]

# === Dashboard (management UI, started with `osa start --with-ui`) ===
# === Dashboard (management UI, started with `osa start`) ===
# The dashboard mints its archive token with JWT_SECRET (shared with the
# server) and signs its session cookie with SESSION_SECRET. Log in with
# DASHBOARD_USERNAME/PASSWORD, or just run `osa dashboard` to open it signed in.
Expand Down Expand Up @@ -367,7 +367,6 @@ def _build_compose_command(
project_dir: Path,
project_name: str,
source: Path | None = None,
profiles: list[str] | None = None,
) -> list[str]:
cmd = [
"docker",
Expand All @@ -388,9 +387,6 @@ def _build_compose_command(
if override_path.exists():
cmd.extend(["-f", str(override_path)])

for profile in profiles or []:
cmd.extend(["--profile", profile])

cmd.extend(["--env-file", str(project_dir / ".env")])

return cmd
Expand Down Expand Up @@ -462,13 +458,11 @@ def start_instance(
project_dir: Path,
detach: bool = True,
source: Path | None = None,
with_ui: bool = True,
osa_version: str | None = None,
ui: UI | None = None,
) -> None:
ui = ui or UI.create()
project_name = _read_project_name(project_dir)
profiles = ["ui"] if with_ui else []

if osa_version is not None:
image_version = osa_version
Expand All @@ -481,7 +475,6 @@ def start_instance(
project_dir=project_dir,
project_name=project_name,
source=source,
profiles=profiles,
)

args = [*cmd, "up"]
Expand Down Expand Up @@ -514,17 +507,16 @@ def start_instance(
hint="Run `osa logs server --tail 50` for details",
)
ui.success(f"OSA {image_version} running", arrow=LOCAL_SERVER_URL)
if with_ui:
# Report the actual configured port, not the default — the operator may
# have overridden it in .env. `or` (not a get-default) so a present-but-
# empty value falls back like docker compose's `${DASHBOARD_PORT:-8081}`.
dashboard_port = (
_read_env_file(project_dir / ".env").get("DASHBOARD_PORT") or "8081"
)
ui.info(
f"Dashboard http://localhost:{dashboard_port}"
" · run `osa dashboard` to open it signed in"
)
# Report the actual configured port, not the default — the operator may
# have overridden it in .env. `or` (not a get-default) so a present-but-
# empty value falls back like docker compose's `${DASHBOARD_PORT:-8081}`.
dashboard_port = (
_read_env_file(project_dir / ".env").get("DASHBOARD_PORT") or "8081"
)
ui.info(
f"Dashboard http://localhost:{dashboard_port}"
" · run `osa dashboard` to open it signed in"
)


def open_dashboard(*, project_dir: Path, ui: UI | None = None) -> None:
Expand Down
9 changes: 1 addition & 8 deletions osa/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,18 +314,12 @@ def start(
Optional[Path],
typer.Option("--source", help="Path to OSA server source for dev mode."),
] = None,
no_ui: Annotated[
bool,
typer.Option(
"--no-ui", help="Start only the API, without the web UI + dashboard."
),
] = False,
osa_version: Annotated[
Optional[str],
typer.Option("--osa-version", help="OSA server image version tag."),
] = None,
) -> None:
"""Start the local OSA instance (API + web UI + dashboard)."""
"""Start the local OSA instance (API + dashboard)."""
from osa.cli.instance import InstanceError, start_instance

ui = _ui(ctx)
Expand All @@ -334,7 +328,6 @@ def start(
project_dir=Path.cwd(),
detach=detach,
source=source.resolve() if source else None,
with_ui=not no_ui,
osa_version=osa_version,
ui=ui,
)
Expand Down
29 changes: 13 additions & 16 deletions osa/cli/templates/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,19 @@ services:
start_period: 5s
restart: unless-stopped

# The web app is out of date and excluded from `osa start` for now — it sits
# in its own `web` profile (the default `ui` profile is server + dashboard).
web:
image: ghcr.io/opensciencearchive/osa-web:${OSA_IMAGE_VERSION:-latest}
environment:
API_URL: http://server:8000
ports:
- "${WEB_PORT:-8080}:3000"
depends_on:
server:
condition: service_healthy
profiles:
- web
restart: unless-stopped
# The end-user web app is out of date, so `osa start` does not run it. Kept
# here (commented out) to make re-enabling trivial once it's updated: drop the
# comments and it joins the stack like every other service.
# web:
# image: ghcr.io/opensciencearchive/osa-web:${OSA_IMAGE_VERSION:-latest}
# environment:
# API_URL: http://server:8000
# ports:
# - "${WEB_PORT:-8080}:3000"
# depends_on:
# server:
# condition: service_healthy
# restart: unless-stopped

# Management dashboard. Logs in with the dashboard credential, then mints a
# SUPERADMIN token signed with the SHARED JWT_SECRET (same value the server
Expand All @@ -107,8 +106,6 @@ services:
depends_on:
server:
condition: service_healthy
profiles:
- ui
restart: unless-stopped

volumes:
Expand Down
54 changes: 19 additions & 35 deletions tests/test_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,13 +372,11 @@ def test_without_override_file(self, tmp_path: Path) -> None:
cmd = _build_compose_command(project_dir=tmp_path, project_name="test")
assert "docker-compose.override.yml" not in " ".join(cmd)

def test_with_profiles(self, tmp_path: Path) -> None:
def test_no_profiles(self, tmp_path: Path) -> None:
# Profiles are gone — every service in the template is always managed.
_write_osa_yaml(tmp_path)
cmd = _build_compose_command(
project_dir=tmp_path, project_name="test", profiles=["ui"]
)
idx = cmd.index("--profile")
assert cmd[idx + 1] == "ui"
cmd = _build_compose_command(project_dir=tmp_path, project_name="test")
assert "--profile" not in cmd

def test_source_adds_dev_override(self, tmp_path: Path) -> None:
_write_osa_yaml(tmp_path)
Expand Down Expand Up @@ -492,51 +490,39 @@ def test_with_source_adds_build(self, tmp_path: Path) -> None:
args = mock_run.call_args[0][0]
assert "--build" in args

def test_source_still_includes_ui(self, tmp_path: Path) -> None:
# `--source` builds the server from source but must still bring up the
# UI profile (web + dashboard) — it's independent of the source build.
def test_uses_no_profiles(self, tmp_path: Path) -> None:
# Profiles are gone — `osa start` manages every service in the template.
_write_osa_yaml(tmp_path)
with _mock_streamed() as mock_run:
start_instance(project_dir=tmp_path, osa_version="v0.0.0")
assert "--profile" not in mock_run.call_args[0][0]

def test_source_uses_no_profiles(self, tmp_path: Path) -> None:
# `--source` builds the server from source and still runs the whole
# stack (dashboard included) without any profile gating.
_write_osa_yaml(tmp_path)
source = tmp_path / "server-src"
source.mkdir()
with _mock_streamed() as mock_run:
start_instance(project_dir=tmp_path, source=source, osa_version="v0.0.0")
args = mock_run.call_args[0][0]
assert "--build" in args
assert "--profile" in args
assert args[args.index("--profile") + 1] == "ui"

def test_ui_profile_is_on_by_default(self, tmp_path: Path) -> None:
# `osa start` brings up the web UI + dashboard by default.
_write_osa_yaml(tmp_path)
with _mock_streamed() as mock_run:
start_instance(project_dir=tmp_path, osa_version="v0.0.0")
args = mock_run.call_args[0][0]
assert "--profile" in args
assert args[args.index("--profile") + 1] == "ui"

def test_no_ui_omits_profile(self, tmp_path: Path) -> None:
# `osa start --no-ui` (with_ui=False) starts only the API.
_write_osa_yaml(tmp_path)
with _mock_streamed() as mock_run:
start_instance(project_dir=tmp_path, with_ui=False, osa_version="v0.0.0")
assert "--profile" not in mock_run.call_args[0][0]
assert "--profile" not in args

def test_with_ui_reports_dashboard_port(self, tmp_path: Path) -> None:
def test_reports_dashboard_port(self, tmp_path: Path) -> None:
# The printed dashboard URL must reflect the .env port override.
from unittest.mock import MagicMock

_write_osa_yaml(tmp_path)
(tmp_path / ".env").write_text("JWT_SECRET=x\nDASHBOARD_PORT=9091\n")
ui = MagicMock()
with _mock_streamed():
start_instance(
project_dir=tmp_path, with_ui=True, osa_version="v0.0.0", ui=ui
)
start_instance(project_dir=tmp_path, osa_version="v0.0.0", ui=ui)
printed = " ".join(str(call) for call in ui.info.call_args_list)
assert "9091" in printed # DASHBOARD_PORT
assert "Web UI" not in printed # web is excluded from `osa start`

def test_with_ui_empty_port_falls_back_like_compose(self, tmp_path: Path) -> None:
def test_empty_port_falls_back_like_compose(self, tmp_path: Path) -> None:
# A present-but-empty port must fall back to the default (as compose's
# `:-` does), not print a blank port.
from unittest.mock import MagicMock
Expand All @@ -545,9 +531,7 @@ def test_with_ui_empty_port_falls_back_like_compose(self, tmp_path: Path) -> Non
(tmp_path / ".env").write_text("JWT_SECRET=x\nDASHBOARD_PORT=\n")
ui = MagicMock()
with _mock_streamed():
start_instance(
project_dir=tmp_path, with_ui=True, osa_version="v0.0.0", ui=ui
)
start_instance(project_dir=tmp_path, osa_version="v0.0.0", ui=ui)
printed = " ".join(str(call) for call in ui.info.call_args_list)
assert "localhost:8081" in printed

Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading