Skip to content

Commit f22edcd

Browse files
omer9564claude
andcommitted
Add control plane connectivity control for offline mode
Adds PDP-level bindings for OPAL's new server connectivity feature, allowing the PDP to start disconnected from the control plane and serve from a local backup. Includes runtime HTTP endpoints (/control-plane/connectivity) authenticated with the PDP API key. - Add PDP_CONTROL_PLANE_CONNECTIVITY_DISABLED config option - Create /control-plane/connectivity GET/enable/disable endpoints - Upgrade opal-common and opal-client to 0.9.4rc6 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8dd5a7a commit f22edcd

5 files changed

Lines changed: 110 additions & 2 deletions

File tree

horizon/config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@ def __new__(cls, *, prefix=None, is_model=True): # noqa: ARG004
131131
description="Filename for offline mode's policy backup (OPAL's offline mode backup)",
132132
)
133133

134+
CONTROL_PLANE_CONNECTIVITY_DISABLED = confi.bool(
135+
"CONTROL_PLANE_CONNECTIVITY_DISABLED",
136+
False,
137+
description="When true (and ENABLE_OFFLINE_MODE is true), the PDP starts disconnected from the control plane "
138+
"and serves from a local backup. Can be toggled at runtime via the /control-plane/connectivity endpoints.",
139+
)
140+
134141
CONFIG_FETCH_MAX_RETRIES = confi.int(
135142
"CONFIG_FETCH_MAX_RETRIES",
136143
6,

horizon/connectivity/__init__.py

Whitespace-only changes.

horizon/connectivity/api.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
from fastapi import APIRouter, Depends, HTTPException, status
6+
from pydantic import BaseModel
7+
8+
from horizon.authentication import enforce_pdp_token
9+
10+
if TYPE_CHECKING:
11+
from opal_client.client import OpalClient
12+
13+
14+
class ConnectivityStatus(BaseModel):
15+
control_plane_connectivity_disabled: bool
16+
offline_mode_enabled: bool
17+
18+
19+
class ConnectivityActionResult(BaseModel):
20+
status: str
21+
22+
23+
def init_connectivity_router(opal_client: OpalClient):
24+
router = APIRouter(
25+
prefix="/control-plane",
26+
dependencies=[Depends(enforce_pdp_token)],
27+
)
28+
29+
@router.get(
30+
"/connectivity",
31+
status_code=status.HTTP_200_OK,
32+
response_model=ConnectivityStatus,
33+
summary="Get control plane connectivity status",
34+
description="Returns the current connectivity state to the control plane and whether offline mode is enabled.",
35+
)
36+
async def get_connectivity_status():
37+
return ConnectivityStatus(
38+
control_plane_connectivity_disabled=opal_client.opal_server_connectivity_disabled,
39+
offline_mode_enabled=opal_client.offline_mode_enabled,
40+
)
41+
42+
@router.post(
43+
"/connectivity/enable",
44+
status_code=status.HTTP_200_OK,
45+
response_model=ConnectivityActionResult,
46+
summary="Enable control plane connectivity",
47+
description="Starts the policy and data updaters, reconnecting to the control plane. "
48+
"Triggers a full rehydration (policy refetch + data refetch). "
49+
"Requires offline mode to be enabled.",
50+
)
51+
async def enable_connectivity():
52+
if not opal_client.offline_mode_enabled:
53+
raise HTTPException(
54+
status_code=status.HTTP_400_BAD_REQUEST,
55+
detail="Cannot enable control plane connectivity: offline mode is not enabled",
56+
)
57+
if not opal_client.opal_server_connectivity_disabled:
58+
return ConnectivityActionResult(status="already_enabled")
59+
60+
await opal_client.enable_opal_server_connectivity()
61+
return ConnectivityActionResult(status="enabled")
62+
63+
@router.post(
64+
"/connectivity/disable",
65+
status_code=status.HTTP_200_OK,
66+
response_model=ConnectivityActionResult,
67+
summary="Disable control plane connectivity",
68+
description="Stops the policy and data updaters, disconnecting from the control plane. "
69+
"Requires offline mode to be enabled. The policy store continues serving from its current state.",
70+
)
71+
async def disable_connectivity():
72+
if not opal_client.offline_mode_enabled:
73+
raise HTTPException(
74+
status_code=status.HTTP_400_BAD_REQUEST,
75+
detail="Cannot disable control plane connectivity: offline mode is not enabled",
76+
)
77+
if opal_client.opal_server_connectivity_disabled:
78+
return ConnectivityActionResult(status="already_disabled")
79+
80+
await opal_client.disable_opal_server_connectivity()
81+
return ConnectivityActionResult(status="disabled")
82+
83+
return router

horizon/pdp.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
from horizon.authentication import enforce_pdp_token
2929
from horizon.config import MOCK_API_KEY, sidecar_config
30+
from horizon.connectivity.api import init_connectivity_router
3031
from horizon.enforcer.api import init_enforcer_api_router, stats_manager
3132
from horizon.enforcer.opa.config_maker import (
3233
get_opa_authz_policy_file_path,
@@ -148,6 +149,7 @@ def __init__(self):
148149

149150
self._configure_opal_data_updater()
150151
self._configure_opal_offline_mode()
152+
self._configure_opal_server_connectivity()
151153

152154
if sidecar_config.PRINT_CONFIG_ON_STARTUP:
153155
logger.info(
@@ -326,6 +328,16 @@ def _configure_opal_offline_mode(self):
326328
Path(sidecar_config.OFFLINE_MODE_BACKUP_DIR) / sidecar_config.OFFLINE_MODE_POLICY_BACKUP_FILENAME
327329
)
328330

331+
def _configure_opal_server_connectivity(self):
332+
"""
333+
configure control plane connectivity when offline mode is enabled.
334+
When both offline mode and connectivity disabled are set, the PDP starts
335+
disconnected from the control plane and serves from a local backup.
336+
"""
337+
opal_client_config.DEFAULT_OPAL_SERVER_CONNECTIVITY_DISABLED = (
338+
sidecar_config.ENABLE_OFFLINE_MODE and sidecar_config.CONTROL_PLANE_CONNECTIVITY_DISABLED
339+
)
340+
329341
def _fix_data_topics(self) -> list[str]:
330342
"""
331343
This is a worksaround for the following issue:
@@ -411,6 +423,12 @@ def _configure_api_routes(self, app: FastAPI):
411423
include_in_schema=False,
412424
dependencies=[Depends(enforce_pdp_token)],
413425
)
426+
connectivity_router = init_connectivity_router(self._opal)
427+
app.include_router(
428+
connectivity_router,
429+
tags=["Control Plane Connectivity"],
430+
dependencies=[Depends(enforce_pdp_token)],
431+
)
414432

415433
# TODO: remove this when clients update sdk version (legacy routes)
416434
@app.post(

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ httpx>=0.27.0,<1
1717
# google-re2 # use re2 instead of re for regex matching because it's simiplier and safer for user inputted regexes
1818
protobuf>=6.33.5 # pinned to avoid CVE-2026-0994
1919
cryptography>=46.0.5,<47 # pinned to avoid CVE-2026-26007
20-
opal-common==0.8.3
21-
opal-client==0.8.3
20+
opal-common==0.9.4rc6
21+
opal-client==0.9.4rc6

0 commit comments

Comments
 (0)