fix: serve legacy route aliases during cutover
This commit is contained in:
parent
8ab1d0c09a
commit
14befd0b50
4 changed files with 57 additions and 2 deletions
|
|
@ -9,6 +9,7 @@ from uuid import uuid4
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from fastapi import APIRouter, Header, HTTPException, Request, status
|
from fastapi import APIRouter, Header, HTTPException, Request, status
|
||||||
|
from fastapi.routing import APIRoute
|
||||||
from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse
|
||||||
|
|
||||||
from hub_core.runtime.compat_catalogs import (
|
from hub_core.runtime.compat_catalogs import (
|
||||||
|
|
@ -504,6 +505,25 @@ def create_compatibility_router() -> APIRouter:
|
||||||
"<p>Compatibility runtime candidate; projections are authoritative.</p>"
|
"<p>Compatibility runtime candidate; projections are authoritative.</p>"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Core Hub historically served the same compatibility operations both below
|
||||||
|
# /api/v2 and at the root. Keep the aliases as real routes (not merely
|
||||||
|
# OpenAPI aliases) so a route-group cutover cannot turn existing clients into
|
||||||
|
# 404s. The canonical /api/v2 routes remain the only schema-bearing routes;
|
||||||
|
# openapi_json adds the documented alias paths deterministically.
|
||||||
|
for route in list(router.routes):
|
||||||
|
if not isinstance(route, APIRoute) or not route.path.startswith("/api/v2/"):
|
||||||
|
continue
|
||||||
|
alias = route.path.removeprefix("/api/v2")
|
||||||
|
router.add_api_route(
|
||||||
|
alias,
|
||||||
|
route.endpoint,
|
||||||
|
methods=sorted(route.methods or []),
|
||||||
|
status_code=route.status_code,
|
||||||
|
response_class=route.response_class,
|
||||||
|
include_in_schema=False,
|
||||||
|
name=f"legacy_{route.name}",
|
||||||
|
)
|
||||||
|
|
||||||
return router
|
return router
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,11 +69,24 @@ class RuntimeSettings:
|
||||||
|
|
||||||
def readiness_checks(self, store_backend: str) -> dict[str, str]:
|
def readiness_checks(self, store_backend: str) -> dict[str, str]:
|
||||||
ephemeral_allowed = store_backend != "memory" or self.allow_ephemeral
|
ephemeral_allowed = store_backend != "memory" or self.allow_ephemeral
|
||||||
|
protected_groups = self.v2_groups & {
|
||||||
|
"registry",
|
||||||
|
"credentials",
|
||||||
|
"interaction",
|
||||||
|
"deferred",
|
||||||
|
"operator",
|
||||||
|
}
|
||||||
|
authorization_ready = (
|
||||||
|
not protected_groups
|
||||||
|
or bool(self.api_token)
|
||||||
|
or store_backend == "postgresql"
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"environment": self.environment,
|
"environment": self.environment,
|
||||||
"configured_backend": self.backend,
|
"configured_backend": self.backend,
|
||||||
"active_backend": store_backend,
|
"active_backend": store_backend,
|
||||||
"ephemeral_backend": "allowed" if ephemeral_allowed else "not_allowed",
|
"ephemeral_backend": "allowed" if ephemeral_allowed else "not_allowed",
|
||||||
|
"authorization": "ok" if authorization_ready else "unavailable",
|
||||||
"contract": "helixforge.hub-extension/0.1.0",
|
"contract": "helixforge.hub-extension/0.1.0",
|
||||||
"v2_groups": ",".join(sorted(self.v2_groups)) or "none",
|
"v2_groups": ",".join(sorted(self.v2_groups)) or "none",
|
||||||
"v2_write_groups": ",".join(sorted(self.v2_write_groups)) or "none",
|
"v2_write_groups": ",".join(sorted(self.v2_write_groups)) or "none",
|
||||||
|
|
@ -81,6 +94,9 @@ class RuntimeSettings:
|
||||||
}
|
}
|
||||||
|
|
||||||
def is_ready(self, store_backend: str) -> bool:
|
def is_ready(self, store_backend: str) -> bool:
|
||||||
return self.backend == store_backend and (
|
checks = self.readiness_checks(store_backend)
|
||||||
store_backend != "memory" or self.allow_ephemeral
|
return (
|
||||||
|
self.backend == store_backend
|
||||||
|
and (store_backend != "memory" or self.allow_ephemeral)
|
||||||
|
and checks["authorization"] == "ok"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,10 @@ def test_system_compatibility_and_protected_auth(tmp_path) -> None:
|
||||||
document = client.get("/api/v2/openapi.json").json()
|
document = client.get("/api/v2/openapi.json").json()
|
||||||
assert "/api/v2/hubs" in document["paths"]
|
assert "/api/v2/hubs" in document["paths"]
|
||||||
assert "/hubs" in document["paths"]
|
assert "/hubs" in document["paths"]
|
||||||
|
assert client.get("/widget-types").status_code == 200
|
||||||
|
assert client.get("/hubs").status_code == 401
|
||||||
|
assert client.get("/hubs", headers=HEADERS).status_code == 200
|
||||||
|
assert client.get("/console", headers=HEADERS).status_code == 200
|
||||||
asyncio.run(store.aclose())
|
asyncio.run(store.aclose())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,21 @@ def test_production_readiness_fails_closed_for_ephemeral_backend() -> None:
|
||||||
assert response.json()["checks"]["ephemeral_backend"] == "not_allowed"
|
assert response.json()["checks"]["ephemeral_backend"] == "not_allowed"
|
||||||
|
|
||||||
|
|
||||||
|
def test_protected_compatibility_readiness_requires_authorization_dependency() -> None:
|
||||||
|
settings = RuntimeSettings(
|
||||||
|
environment="test",
|
||||||
|
backend="memory",
|
||||||
|
allow_ephemeral=True,
|
||||||
|
v2_groups=frozenset({"registry"}),
|
||||||
|
)
|
||||||
|
response = TestClient(
|
||||||
|
create_app(settings=settings, port_store=InMemoryPortStore())
|
||||||
|
).get("/readyz")
|
||||||
|
|
||||||
|
assert response.status_code == 503
|
||||||
|
assert response.json()["checks"]["authorization"] == "unavailable"
|
||||||
|
|
||||||
|
|
||||||
def test_registry_validates_and_registers_idempotently() -> None:
|
def test_registry_validates_and_registers_idempotently() -> None:
|
||||||
runtime = client()
|
runtime = client()
|
||||||
package = ops_hub_package()
|
package = ops_hub_package()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue