Skip to content

Commit 2f88093

Browse files
committed
Distinguish Iris dashboard titles
1 parent 28163ea commit 2f88093

6 files changed

Lines changed: 133 additions & 7 deletions

File tree

lib/iris/src/iris/cluster/dashboard_common.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@
99
"""
1010

1111
import logging
12+
import os
13+
import re
1214
from collections.abc import AsyncGenerator, Awaitable, Callable
1315
from contextlib import AbstractAsyncContextManager, asynccontextmanager
16+
from html import escape
1417
from pathlib import Path
1518
from typing import Any
1619

@@ -63,6 +66,7 @@ async def _lifespan(_app: Starlette) -> AsyncGenerator[None, None]:
6366

6467
# Allow browsers to cache static assets for up to 10 minutes before revalidating.
6568
STATIC_MAX_AGE_SECONDS = 600
69+
DASHBOARD_TITLE_ENV_VAR = "IRIS_DASHBOARD_TITLE"
6670

6771

6872
class _CacheControlStaticFiles:
@@ -163,6 +167,40 @@ def favicon_route() -> Route:
163167
"""
164168

165169

170+
def dashboard_title_from_config(config: Any | None) -> str | None:
171+
"""Return a human-readable dashboard title from cluster config."""
172+
if config is None:
173+
return None
174+
platform = getattr(config, "platform", None)
175+
for value in (getattr(config, "name", ""), getattr(platform, "label_prefix", "")):
176+
if value is None:
177+
continue
178+
title = str(value).strip()
179+
if title:
180+
return title
181+
return None
182+
183+
184+
def _configured_dashboard_title(dashboard_title: str | None) -> str | None:
185+
title = dashboard_title if dashboard_title is not None else os.environ.get(DASHBOARD_TITLE_ENV_VAR, "")
186+
title = title.strip()
187+
return title or None
188+
189+
190+
def _with_dashboard_title(html: str, dashboard_title: str | None) -> str:
191+
title = _configured_dashboard_title(dashboard_title)
192+
if title is None:
193+
return html
194+
escaped_title = escape(f"{title} | Iris", quote=False)
195+
return re.sub(
196+
r"<title>.*?</title>",
197+
f"<title>{escaped_title}</title>",
198+
html,
199+
count=1,
200+
flags=re.IGNORECASE | re.DOTALL,
201+
)
202+
203+
166204
def html_shell(title: str, dashboard_type: str = "controller") -> str:
167205
"""Return the pre-built HTML page for a dashboard.
168206
@@ -178,4 +216,4 @@ def html_shell(title: str, dashboard_type: str = "controller") -> str:
178216
if not index_path.exists():
179217
logger.warning("Dashboard HTML %s not found; serving placeholder", index_path)
180218
return _NOT_BUILT_HTML
181-
return index_path.read_text()
219+
return _with_dashboard_title(index_path.read_text(), None)

lib/iris/src/iris/cluster/providers/gcp/bootstrap.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
import json
1414
import logging
1515
import re
16+
import shlex
1617

1718
import yaml
1819
from google.protobuf.json_format import MessageToDict
1920

2021
from collections.abc import Callable
22+
from iris.cluster.dashboard_common import DASHBOARD_TITLE_ENV_VAR, dashboard_title_from_config
2123
from iris.rpc import config_pb2
2224

2325
logger = logging.getLogger(__name__)
@@ -362,6 +364,7 @@ def build_worker_bootstrap_script(
362364
--ulimit core=0:0 \\
363365
-v /var/cache/iris:/var/cache/iris \\
364366
{{ config_volume }} \\
367+
{{ dashboard_env }} \\
365368
{{ docker_image }} \\
366369
.venv/bin/python -m iris.cluster.controller.main serve \\
367370
--host 0.0.0.0 --port {{ port }} {{ config_flag }} {{ fresh_flag }}
@@ -434,6 +437,7 @@ def build_controller_bootstrap_script(
434437
docker_image: str,
435438
port: int,
436439
config_yaml: str = "",
440+
dashboard_title: str | None = None,
437441
fresh: bool = False,
438442
) -> str:
439443
"""Build bootstrap script for controller VM.
@@ -442,6 +446,7 @@ def build_controller_bootstrap_script(
442446
docker_image: Docker image to run
443447
port: Controller port
444448
config_yaml: Optional YAML config to write to /etc/iris/config.yaml
449+
dashboard_title: Optional browser title prefix for the dashboard
445450
fresh: When True, pass ``--fresh`` to the controller serve command so
446451
it starts with an empty local database and skips checkpoint restore.
447452
"""
@@ -453,6 +458,8 @@ def build_controller_bootstrap_script(
453458
config_setup = "# No config file provided"
454459
config_volume = ""
455460
config_flag = ""
461+
dashboard_title = (dashboard_title or "").strip()
462+
dashboard_env = f"-e {DASHBOARD_TITLE_ENV_VAR}={shlex.quote(dashboard_title)}" if dashboard_title else ""
456463

457464
return render_template(
458465
CONTROLLER_BOOTSTRAP_SCRIPT,
@@ -461,6 +468,7 @@ def build_controller_bootstrap_script(
461468
port=port,
462469
config_setup=config_setup,
463470
config_volume=config_volume,
471+
dashboard_env=dashboard_env,
464472
config_flag=config_flag,
465473
fresh_flag="--fresh" if fresh else "",
466474
)
@@ -493,4 +501,10 @@ def build_controller_bootstrap_script_from_config(
493501

494502
image = resolve_image(image, zone)
495503

496-
return build_controller_bootstrap_script(image, port, config_yaml, fresh=fresh)
504+
return build_controller_bootstrap_script(
505+
image,
506+
port,
507+
config_yaml,
508+
dashboard_title=dashboard_title_from_config(config),
509+
fresh=fresh,
510+
)

lib/iris/src/iris/cluster/providers/k8s/controller.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from urllib.parse import urlparse
2222

2323
from iris.cluster.config import config_to_dict
24+
from iris.cluster.dashboard_common import DASHBOARD_TITLE_ENV_VAR, dashboard_title_from_config
2425
from iris.cluster.providers.k8s.service import K8sService
2526
from iris.cluster.providers.k8s.types import K8sResource
2627
from iris.cluster.providers.types import InfraError, Labels
@@ -135,6 +136,7 @@ def _build_controller_deployment(
135136
port: int,
136137
node_selector: dict[str, str],
137138
s3_env_vars: list[dict],
139+
dashboard_title: str | None = None,
138140
fresh: bool = False,
139141
) -> dict:
140142
"""Build the controller Deployment manifest as a dict."""
@@ -144,6 +146,9 @@ def _build_controller_deployment(
144146
"requests": {"cpu": _CONTROLLER_CPU_REQUEST, "memory": _CONTROLLER_MEMORY_REQUEST},
145147
"limits": {"cpu": _CONTROLLER_CPU_REQUEST, "memory": _CONTROLLER_MEMORY_REQUEST},
146148
}
149+
env_vars = list(s3_env_vars)
150+
if dashboard_title:
151+
env_vars.append({"name": DASHBOARD_TITLE_ENV_VAR, "value": dashboard_title})
147152
return {
148153
"apiVersion": "apps/v1",
149154
"kind": "Deployment",
@@ -176,7 +181,7 @@ def _build_controller_deployment(
176181
*(["--fresh"] if fresh else []),
177182
],
178183
"ports": [{"containerPort": port}],
179-
"env": s3_env_vars,
184+
"env": env_vars,
180185
"securityContext": {"capabilities": {"add": ["SYS_PTRACE"]}},
181186
"resources": controller_resources,
182187
"volumeMounts": [
@@ -311,6 +316,7 @@ def start_controller(self, config: config_pb2.IrisClusterConfig, *, fresh: bool
311316
port=port,
312317
node_selector={self._iris_labels.iris_scale_group: cw.scale_group},
313318
s3_env_vars=s3_env,
319+
dashboard_title=dashboard_title_from_config(config),
314320
fresh=fresh,
315321
)
316322
self._kubectl.apply_json(deploy_manifest)

lib/iris/tests/cluster/providers/gcp/test_bootstrap.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
rewrite_ghcr_to_ar_remote,
1515
zone_to_multi_region,
1616
)
17+
from iris.cluster.dashboard_common import DASHBOARD_TITLE_ENV_VAR
1718
from iris.rpc import config_pb2
1819

1920

@@ -157,6 +158,19 @@ def resolve_image(image: str, zone: str | None = None) -> str:
157158
assert 'sudo gcloud auth configure-docker "$AR_HOST" -q || true' in script
158159

159160

161+
def test_build_controller_bootstrap_script_sets_dashboard_title() -> None:
162+
config = config_pb2.IrisClusterConfig()
163+
config.platform.label_prefix = "marin-dev"
164+
config.controller.image = "ghcr.io/marin-community/iris-controller:latest"
165+
config.controller.gcp.zone = "us-central1-a"
166+
config.controller.gcp.port = 10000
167+
config.platform.gcp.project_id = "hai-gcp-models"
168+
169+
script = build_controller_bootstrap_script_from_config(config, resolve_image=lambda image, zone=None: image)
170+
171+
assert f"-e {DASHBOARD_TITLE_ENV_VAR}=marin-dev" in script
172+
173+
160174
# --- GcpWorkerProvider.resolve_image() tests ---
161175

162176

lib/iris/tests/cluster/providers/k8s/test_coreweave.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
_CONTROLLER_CPU_REQUEST,
2323
_CONTROLLER_MEMORY_REQUEST,
2424
)
25+
from iris.cluster.dashboard_common import DASHBOARD_TITLE_ENV_VAR
2526
from iris.cluster.providers.k8s.fake import InMemoryK8sService
2627
from iris.cluster.providers.k8s.types import K8sResource
2728
from iris.cluster.providers.types import (
@@ -163,10 +164,11 @@ def test_start_controller_creates_all_resources():
163164

164165
# Verify controller uses S3 env vars (no GCS credentials)
165166
container = deploy_spec["template"]["spec"]["containers"][0]
166-
env_names = [e["name"] for e in container["env"]]
167-
assert "AWS_ACCESS_KEY_ID" in env_names
168-
assert "AWS_SECRET_ACCESS_KEY" in env_names
169-
assert "GOOGLE_APPLICATION_CREDENTIALS" not in env_names
167+
env_by_name = {e["name"]: e for e in container["env"]}
168+
assert "AWS_ACCESS_KEY_ID" in env_by_name
169+
assert "AWS_SECRET_ACCESS_KEY" in env_by_name
170+
assert "GOOGLE_APPLICATION_CREDENTIALS" not in env_by_name
171+
assert env_by_name[DASHBOARD_TITLE_ENV_VAR]["value"] == "iris"
170172
assert container["resources"]["requests"] == {"cpu": _CONTROLLER_CPU_REQUEST, "memory": _CONTROLLER_MEMORY_REQUEST}
171173
assert container["resources"]["limits"] == {"cpu": _CONTROLLER_CPU_REQUEST, "memory": _CONTROLLER_MEMORY_REQUEST}
172174

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Copyright The Marin Authors
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from iris.cluster import dashboard_common
5+
from iris.rpc import config_pb2
6+
7+
8+
def test_dashboard_title_from_config_prefers_name() -> None:
9+
config = config_pb2.IrisClusterConfig()
10+
config.name = "marin"
11+
config.platform.label_prefix = "marin-dev"
12+
13+
assert dashboard_common.dashboard_title_from_config(config) == "marin"
14+
15+
16+
def test_dashboard_title_from_config_falls_back_to_label_prefix() -> None:
17+
config = config_pb2.IrisClusterConfig()
18+
config.platform.label_prefix = "cw"
19+
20+
assert dashboard_common.dashboard_title_from_config(config) == "cw"
21+
22+
23+
def test_html_shell_uses_deployed_dashboard_title(monkeypatch, tmp_path) -> None:
24+
dist = tmp_path / "dist"
25+
dist.mkdir()
26+
(dist / "controller.html").write_text(
27+
"<!doctype html><html><head><title>Iris Dashboard</title></head><body></body></html>"
28+
)
29+
30+
monkeypatch.setattr(dashboard_common, "VUE_DIST_DIR", dist)
31+
monkeypatch.setattr(dashboard_common, "DOCKER_VUE_DIST_DIR", tmp_path / "missing")
32+
monkeypatch.setenv(dashboard_common.DASHBOARD_TITLE_ENV_VAR, "marin-dev")
33+
34+
html = dashboard_common.html_shell("Iris Controller", "controller")
35+
36+
assert "<title>marin-dev | Iris</title>" in html
37+
38+
39+
def test_html_shell_escapes_deployed_dashboard_title(monkeypatch, tmp_path) -> None:
40+
dist = tmp_path / "dist"
41+
dist.mkdir()
42+
(dist / "controller.html").write_text(
43+
"<!doctype html><html><head><title>Iris Dashboard</title></head><body></body></html>"
44+
)
45+
46+
monkeypatch.setattr(dashboard_common, "VUE_DIST_DIR", dist)
47+
monkeypatch.setattr(dashboard_common, "DOCKER_VUE_DIST_DIR", tmp_path / "missing")
48+
monkeypatch.setenv(dashboard_common.DASHBOARD_TITLE_ENV_VAR, "<cw>")
49+
50+
html = dashboard_common.html_shell("Iris Controller", "controller")
51+
52+
assert "<title>&lt;cw&gt; | Iris</title>" in html

0 commit comments

Comments
 (0)