Skip to content

Commit 6e55eb0

Browse files
committed
fix: make opa local setup more resilient
1 parent ca553ae commit 6e55eb0

16 files changed

Lines changed: 726 additions & 22 deletions

File tree

.github/actions/build-policy-wasm/action.yaml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,6 @@ description: >
88
runs:
99
using: composite
1010
steps:
11-
- name: Install OPA
12-
shell: bash
13-
env:
14-
OPA_VERSION: v1.8.0
15-
run: |
16-
curl -fsSL -o /usr/local/bin/opa \
17-
"https://github.com/open-policy-agent/opa/releases/download/${OPA_VERSION}/opa_linux_amd64_static"
18-
chmod +x /usr/local/bin/opa
1911
- name: Build policy WASM
2012
shell: bash
2113
run: ./script/build_policy_wasm.sh

docs/auth/authorization/policy-engine.mdx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,23 @@ The embedded PDP uses a WASM-compiled Rego policy engine built into the auth ser
2424

2525
### How It Works
2626

27+
- Rego policy files are compiled to `policy.wasm` with `make build-policy`
2728
- Policy data (role bindings, workspace visibility, endpoint permissions) is served by the auth service
2829
- Data is refreshed on a configurable interval (default: 30 seconds)
2930

31+
In source checkouts, startup with `auth.enabled: true` and
32+
`policy_decision_point_provider: embedded` automatically builds a missing or
33+
stale `policy.wasm` using the pinned OPA version from `script/build_policy_wasm.sh`.
34+
Packaged wheels and container images should include `policy.wasm` at build time.
35+
3036
### Configuration
3137

3238
```yaml
3339
auth:
3440
enabled: true
3541
policy_decision_point_provider: "embedded"
3642
policy_decision_point_base_url: "http://auth:8000"
43+
embedded_pdp_auto_build_wasm: false
3744
policy_data_refresh_interval: 30 # seconds
3845
```
3946

docs/auth/deployment/configuration.mdx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ NMP_AUTH_ENABLED=true
7373
NMP_AUTH_POLICY_DECISION_POINT_BASE_URL=http://auth:8000
7474
NMP_AUTH_POLICY_DECISION_POINT_PROVIDER=embedded
7575
NMP_AUTH_ADMIN_EMAIL=admin@example.com
76+
NMP_AUTH_EMBEDDED_PDP_AUTO_BUILD_WASM=true
7677
```
7778

7879
Nested keys (e.g., OIDC) use double underscore: `NMP_AUTH_OIDC__ISSUER`, `NMP_AUTH_OIDC__CLIENT_ID`.
@@ -93,16 +94,38 @@ auth:
9394
enabled: true
9495
policy_decision_point_provider: embedded
9596
policy_decision_point_base_url: "http://localhost:8080"
97+
embedded_pdp_auto_build_wasm: true
9698
admin_email: "admin@example.com"
9799
```
98100

101+
When running from a source checkout, embedded PDP startup automatically builds a
102+
missing or stale `policy.wasm` with the pinned OPA version used by
103+
`make build-policy`. Packaged deployments should include `policy.wasm` at image
104+
or wheel build time; set `embedded_pdp_auto_build_wasm: false` to fail fast if
105+
that artifact is missing.
106+
107+
In offline development environments, provide the pinned OPA binary explicitly:
108+
109+
```bash
110+
OPA_BIN=/path/to/opa_linux_amd64_static uv run nemo services run --host 127.0.0.1 --port 8080
111+
```
112+
113+
Or seed the local cache used by `script/build_policy_wasm.sh`:
114+
115+
```bash
116+
mkdir -p .cache/opa/v1.8.0
117+
cp /path/to/opa_linux_amd64_static .cache/opa/v1.8.0/opa_linux_amd64_static
118+
chmod +x .cache/opa/v1.8.0/opa_linux_amd64_static
119+
```
120+
99121
### Production with embedded PDP
100122

101123
```yaml
102124
auth:
103125
enabled: true
104126
policy_decision_point_base_url: "http://auth:8000"
105127
policy_decision_point_provider: embedded
128+
embedded_pdp_auto_build_wasm: false
106129
policy_data_refresh_interval: 30
107130
admin_email: "platform-admin@company.com"
108131
oidc:

packages/nmp_common/src/nmp/common/config/base.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,15 @@ class AuthConfig(create_service_config_class("auth")):
210210
),
211211
)
212212

213+
embedded_pdp_auto_build_wasm: bool = Field(
214+
default=True,
215+
description=(
216+
"When auth is enabled with the embedded PDP and policy.wasm is missing or stale, "
217+
"build it automatically from a local NeMo Platform source checkout. Packaged deployments "
218+
"should include policy.wasm at build time and can disable this for fail-fast startup."
219+
),
220+
)
221+
213222
oidc: OIDCConfig = Field(
214223
default_factory=OIDCConfig,
215224
description="OIDC configuration for native token validation.",

packages/nmp_platform_runner/src/nmp/platform_runner/run.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,33 @@
2828

2929
logger = logging.getLogger(__name__)
3030
console = Console()
31+
error_console = Console(stderr=True)
3132

3233

3334
def _startup_phase(name: str, t0: float) -> None:
3435
elapsed_ms = round((time.perf_counter() - t0) * 1000)
3536
logger.info("[STARTUP] %s: %dms", name, elapsed_ms)
3637

3738

39+
def _is_policy_wasm_error(error: Exception) -> bool:
40+
return (
41+
error.__class__.__name__ == "PolicyWasmError"
42+
and error.__class__.__module__ == "nmp.core.auth.app.embedded_pdp.policy_wasm"
43+
)
44+
45+
46+
def _display_policy_wasm_error(error: Exception) -> None:
47+
error_console.print()
48+
error_console.print(
49+
Panel(
50+
str(error),
51+
title="Embedded Auth Policy WASM Startup Failed",
52+
border_style="red",
53+
expand=False,
54+
)
55+
)
56+
57+
3858
def run_controllers_in_threads(
3959
controller_run_funcs: dict[str, Callable],
4060
stop_signal: threading.Event,
@@ -146,6 +166,9 @@ def signal_handler(signum: int, _frame: object) -> None:
146166
except KeyboardInterrupt:
147167
logger.info("Received keyboard interrupt, shutting down")
148168
except Exception as error:
169+
if _is_policy_wasm_error(error):
170+
_display_policy_wasm_error(error)
171+
raise SystemExit(1) from None
149172
logger.exception("Fatal error occurred")
150173
raise SystemExit(1) from error
151174
finally:

packages/nmp_platform_runner/src/nmp/platform_runner/server.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,22 @@ async def dispatch(self, request: Request, call_next) -> Response:
7272
return await call_next(request)
7373

7474

75+
def preflight_embedded_auth_policy_wasm(auth_config) -> None:
76+
"""Ensure local embedded auth PDP has a loadable policy.wasm before serving traffic."""
77+
if not auth_config.enabled or auth_config.policy_decision_point_provider != "embedded":
78+
return
79+
80+
try:
81+
from nmp.core.auth.app.embedded_pdp.policy_wasm import ensure_embedded_policy_wasm
82+
except ImportError as exc:
83+
raise RuntimeError(
84+
"Auth is enabled with the embedded PDP, but the nmp-auth package is not installed. "
85+
"Install nmp-auth or set auth.policy_decision_point_provider='opa'."
86+
) from exc
87+
88+
ensure_embedded_policy_wasm(auto_build=getattr(auth_config, "embedded_pdp_auto_build_wasm", True))
89+
90+
7591
def create_platform_openapi_app() -> FastAPI:
7692
"""Create the platform app used for aggregate OpenAPI generation."""
7793
services = []
@@ -196,13 +212,15 @@ async def root_handler() -> Response:
196212

197213
def run_server(services: list[Service] | None = None, host: str = "0.0.0.0", port: int = 8080) -> None:
198214
"""Run the platform API server."""
215+
preflight_embedded_auth_policy_wasm(get_auth_config())
199216
app = create_app(services or [])
200217
setup_fastapi_instrumentations(app)
201218
uvicorn.run(app, host=host, port=port, log_config=None)
202219

203220

204221
def run_server_with_reload(app_factory: str, host: str = "0.0.0.0", port: int = 8080) -> None:
205222
"""Run the platform API server with uvicorn reload enabled."""
223+
preflight_embedded_auth_policy_wasm(get_auth_config())
206224
reload_dirs = [
207225
"packages/nmp_platform/src",
208226
"services/core",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from nmp.platform_runner import run
5+
from rich.console import Console
6+
7+
8+
class PolicyWasmError(Exception):
9+
pass
10+
11+
12+
PolicyWasmError.__module__ = "nmp.core.auth.app.embedded_pdp.policy_wasm"
13+
14+
15+
def test_policy_wasm_error_is_expected_startup_error():
16+
assert run._is_policy_wasm_error(PolicyWasmError("boom"))
17+
assert not run._is_policy_wasm_error(RuntimeError("boom"))
18+
19+
20+
def test_policy_wasm_error_renders_as_panel(tmp_path, monkeypatch):
21+
stderr = tmp_path / "stderr.txt"
22+
console = Console(file=stderr.open("w"), force_terminal=False, width=100)
23+
monkeypatch.setattr(run, "error_console", console)
24+
25+
run._display_policy_wasm_error(
26+
PolicyWasmError(
27+
"Failed to build embedded auth PDP policy.wasm.\n\n"
28+
"Command:\n"
29+
" script/build_policy_wasm.sh\n\n"
30+
"Offline options:\n"
31+
" OPA_BIN=/path/to/opa ./script/build_policy_wasm.sh"
32+
)
33+
)
34+
35+
output = stderr.read_text()
36+
assert "Embedded Auth Policy WASM Startup Failed" in output
37+
assert "script/build_policy_wasm.sh" in output
38+
assert "OPA_BIN=/path/to/opa" in output

packages/nmp_platform_runner/tests/test_server.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,61 @@ def fake_create_app(services, controller_run_funcs=None, _http_client=None):
7272
assert captured["controller_run_funcs"] == {"agents-deployment": plugin_controller}
7373

7474

75+
def test_embedded_auth_preflight_invokes_policy_wasm_helper(monkeypatch):
76+
calls: list[bool] = []
77+
auth_cfg = AuthConfig(
78+
enabled=True,
79+
policy_decision_point_provider="embedded",
80+
embedded_pdp_auto_build_wasm=False,
81+
)
82+
83+
from nmp.core.auth.app.embedded_pdp import policy_wasm
84+
85+
monkeypatch.setattr(policy_wasm, "ensure_embedded_policy_wasm", lambda *, auto_build: calls.append(auto_build))
86+
87+
server.preflight_embedded_auth_policy_wasm(auth_cfg)
88+
89+
assert calls == [False]
90+
91+
92+
@pytest.mark.parametrize(
93+
"auth_cfg",
94+
[
95+
AuthConfig(enabled=False, policy_decision_point_provider="embedded"),
96+
AuthConfig(enabled=True, policy_decision_point_provider="opa"),
97+
],
98+
)
99+
def test_embedded_auth_preflight_skips_when_not_needed(auth_cfg, monkeypatch):
100+
calls: list[bool] = []
101+
102+
from nmp.core.auth.app.embedded_pdp import policy_wasm
103+
104+
monkeypatch.setattr(policy_wasm, "ensure_embedded_policy_wasm", lambda *, auto_build: calls.append(auto_build))
105+
106+
server.preflight_embedded_auth_policy_wasm(auth_cfg)
107+
108+
assert calls == []
109+
110+
111+
def test_run_server_runs_embedded_auth_preflight():
112+
auth_cfg = _make_auth_config(enabled=True)
113+
calls: list[AuthConfig] = []
114+
with (
115+
patch("nmp.platform_runner.server.get_auth_config", return_value=auth_cfg),
116+
patch(
117+
"nmp.platform_runner.server.preflight_embedded_auth_policy_wasm", side_effect=lambda cfg: calls.append(cfg)
118+
),
119+
patch("nmp.platform_runner.server.create_app", return_value=FastAPI()) as create_app,
120+
patch("nmp.platform_runner.server.setup_fastapi_instrumentations"),
121+
patch("nmp.platform_runner.server.uvicorn.run") as uvicorn_run,
122+
):
123+
server.run_server(services=[], host="127.0.0.1", port=9999)
124+
125+
assert calls == [auth_cfg]
126+
create_app.assert_called_once_with([])
127+
uvicorn_run.assert_called_once()
128+
129+
75130
def test_create_default_app_raises_for_unknown_service_from_env(monkeypatch):
76131
monkeypatch.setattr(server, "_obs_initialized", True)
77132
monkeypatch.setenv("NMP_SERVICES", "missing-service")

0 commit comments

Comments
 (0)