feat(install): route any manifest-known app through the orchestrator
package.install routed to the manifest-driven orchestrator only for a hardcoded per-app allowlist; every other app fell through to the legacy flow, which ignores manifests entirely and creates a bare container with no ports or volumes (strfry crash-looped this way on .228, 2026-07-09). New ContainerOrchestrator::knows_app(app_id) (default false; prod checks its loaded-manifest map, disk + signed-catalog overlay). The install gate is now allowlist OR knows_app — no per-app Rust for manifest-driven apps, matching the packaging invariant. Apps without a manifest keep the legacy flow including the container-exists adopt block; the unknown-app_id → legacy fallback inside the orchestrator branch is unchanged. Tests: knows_app_reflects_loaded_manifests + install suite 37/37. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
707c912606
commit
55c20e0d6e
@ -330,8 +330,20 @@ impl RpcHandler {
|
|||||||
// mode).
|
// mode).
|
||||||
// The adoption block is being phased out as apps move to the
|
// The adoption block is being phased out as apps move to the
|
||||||
// orchestrator path. Non-orchestrator apps still hit it.
|
// orchestrator path. Non-orchestrator apps still hit it.
|
||||||
let orchestrator_managed =
|
let orchestrator_managed = match self.orchestrator.as_ref() {
|
||||||
should_try_orchestrator_install(package_id, self.orchestrator.is_some());
|
// Migration allowlist OR any app whose manifest the orchestrator
|
||||||
|
// knows (disk or signed-catalog overlay). A manifest-driven app
|
||||||
|
// must never fall through to the legacy flow — it ignores the
|
||||||
|
// manifest entirely and creates a bare container with no ports or
|
||||||
|
// volumes (strfry, 2026-07-09).
|
||||||
|
Some(orch) => {
|
||||||
|
should_try_orchestrator_install(package_id, true)
|
||||||
|
|| orch
|
||||||
|
.knows_app(orchestrator_install_app_id(package_id))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
|
||||||
// Check if container already exists (legacy adoption — non-orchestrator
|
// Check if container already exists (legacy adoption — non-orchestrator
|
||||||
// apps only).
|
// apps only).
|
||||||
|
|||||||
@ -3610,6 +3610,10 @@ enum HookOutcome {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl ContainerOrchestrator for ProdContainerOrchestrator {
|
impl ContainerOrchestrator for ProdContainerOrchestrator {
|
||||||
|
async fn knows_app(&self, app_id: &str) -> bool {
|
||||||
|
self.state.read().await.manifests.contains_key(app_id)
|
||||||
|
}
|
||||||
|
|
||||||
async fn install(&self, app_id: &str) -> Result<String> {
|
async fn install(&self, app_id: &str) -> Result<String> {
|
||||||
{
|
{
|
||||||
let mut state = self.state.write().await;
|
let mut state = self.state.write().await;
|
||||||
@ -4679,6 +4683,27 @@ app:
|
|||||||
assert!(!calls.iter().any(|c| c.starts_with("build_image:")));
|
assert!(!calls.iter().any(|c| c.starts_with("build_image:")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn knows_app_reflects_loaded_manifests() {
|
||||||
|
// The RPC install gate routes any app with a known manifest through
|
||||||
|
// the orchestrator (no per-app allowlist). knows_app must be true for
|
||||||
|
// a loaded manifest and false otherwise — a false positive would send
|
||||||
|
// a manifest-less app into `loaded()`'s unknown-app_id error path,
|
||||||
|
// skipping the legacy adopt block; a false negative sends a
|
||||||
|
// manifest-driven app to the legacy flow, which creates a bare
|
||||||
|
// container (strfry, 2026-07-09).
|
||||||
|
let rt = Arc::new(MockRuntime::default());
|
||||||
|
let orch = orch_with(rt).await;
|
||||||
|
assert!(!orch.knows_app("strfry").await);
|
||||||
|
orch.insert_manifest_for_test(
|
||||||
|
pull_manifest("strfry", "docker.io/dockurr/strfry:1.0.4"),
|
||||||
|
PathBuf::from("/tmp/fixtures/strfry"),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(orch.knows_app("strfry").await);
|
||||||
|
assert!(!orch.knows_app("no-such-app").await);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn mempool_core_rpc_host_follows_bitcoin_node() {
|
async fn mempool_core_rpc_host_follows_bitcoin_node() {
|
||||||
// B12: mempool's CORE_RPC_HOST must resolve to whichever Bitcoin node
|
// B12: mempool's CORE_RPC_HOST must resolve to whichever Bitcoin node
|
||||||
|
|||||||
@ -25,6 +25,16 @@ pub trait ContainerOrchestrator: Send + Sync {
|
|||||||
/// to a manifest the orchestrator already knows about.
|
/// to a manifest the orchestrator already knows about.
|
||||||
async fn install(&self, app_id: &str) -> Result<String>;
|
async fn install(&self, app_id: &str) -> Result<String>;
|
||||||
|
|
||||||
|
/// True when this orchestrator holds a manifest for `app_id` (disk or
|
||||||
|
/// signed-catalog overlay) — i.e. `install(app_id)` would not fail with
|
||||||
|
/// "unknown app_id". Lets the RPC layer route any manifest-driven app
|
||||||
|
/// through the orchestrator without a per-app allowlist. Defaults to
|
||||||
|
/// `false` so orchestrators without a manifest registry keep routing
|
||||||
|
/// through the legacy install flow.
|
||||||
|
async fn knows_app(&self, _app_id: &str) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Start an already-created container.
|
/// Start an already-created container.
|
||||||
async fn start(&self, app_id: &str) -> Result<()>;
|
async fn start(&self, app_id: &str) -> Result<()>;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user