feat(container): ContainerOrchestrator trait, RpcHandler uses it in prod
Step 4 of the rust-orchestrator migration. Unifies the container lifecycle
surface behind a single trait so the RPC layer stops caring whether it is
talking to the dev or prod orchestrator.
* New trait core/archipelago/src/container/traits.rs: ContainerOrchestrator
with install / start / stop / restart / remove / upgrade / status / list /
logs / health, all keyed by app_id. Every method is async_trait-based.
* ProdContainerOrchestrator: the lifecycle methods are moved from inherent
impl into the trait impl (avoids name-shadowing recursion). Adoption and
reconcile remain inherent since only main.rs / BootReconciler call them.
* DevContainerOrchestrator: new trait impl that forwards to the existing
Dev-named methods, applying the dev container-name + port-offset rules
internally. New load_manifest_for() helper resolves app_id to
<data_dir>/apps/<app_id>/manifest.yml so trait-level install(app_id)
works in dev too. install_container(manifest, path) stays inherent for
the manifest-path RPC shape.
* RpcHandler now holds Option<Arc<dyn ContainerOrchestrator>> and, when in
dev mode, a separate Option<Arc<DevContainerOrchestrator>> for the
manifest_path install RPC. In prod mode RpcHandler::new() constructs a
ProdContainerOrchestrator and calls load_manifests() at startup.
* All seven container-* RPC guards no longer say dev mode required.
container-install still requires dev mode because its manifest_path
argument has no prod meaning; every other container RPC now works in both
modes via the trait.
BOOT STILL DOES NOT USE THIS. main.rs wire-up (Step 6) and BootReconciler
(Step 5) come next. Until then the prod orchestrator is constructed but nothing
populates /opt/archipelago/apps so it has zero manifests to manage, matching
the pre-Step-4 behaviour.
Verification: cargo build -p archipelago clean (11 expected unused method
warnings for methods not yet wired from main.rs). cargo test -p archipelago:
all 21 container::* tests pass (16 prod_orchestrator + 5 others). 24 other
test failures are pre-existing and unrelated (identity_manager / session /
wallet / mesh / credentials — all independently flaky on file-backed state).
This commit is contained in:
@@ -7,8 +7,13 @@ impl RpcHandler {
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self.orchestrator.as_ref().ok_or_else(|| {
|
||||
anyhow::anyhow!("Container orchestrator not available (dev mode required)")
|
||||
// The `container-install { manifest_path }` RPC is a dev-mode convenience
|
||||
// that points at an arbitrary YAML on disk. Production install happens via
|
||||
// the reconciler (BootReconciler, Step 5) and via the unified
|
||||
// ContainerOrchestrator::install(app_id) trait call, which can be exposed
|
||||
// through a separate `container-install-by-id` RPC when needed.
|
||||
let dev = self.dev_orchestrator.as_ref().ok_or_else(|| {
|
||||
anyhow::anyhow!("container-install with manifest_path is only available in dev mode")
|
||||
})?;
|
||||
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
@@ -45,7 +50,7 @@ impl RpcHandler {
|
||||
let manifest: archipelago_container::AppManifest =
|
||||
serde_yaml::from_str(&manifest_content).context("Failed to parse manifest")?;
|
||||
|
||||
let container_name = orchestrator
|
||||
let container_name = dev
|
||||
.install_container(&manifest, manifest_path)
|
||||
.await
|
||||
.context("Failed to install container")?;
|
||||
@@ -57,9 +62,10 @@ impl RpcHandler {
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self.orchestrator.as_ref().ok_or_else(|| {
|
||||
anyhow::anyhow!("Container orchestrator not available (dev mode required)")
|
||||
})?;
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available"))?;
|
||||
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let app_id = params
|
||||
@@ -69,7 +75,7 @@ impl RpcHandler {
|
||||
validate_app_id(app_id)?;
|
||||
|
||||
orchestrator
|
||||
.start_container(app_id)
|
||||
.start(app_id)
|
||||
.await
|
||||
.context("Failed to start container")?;
|
||||
|
||||
@@ -80,9 +86,10 @@ impl RpcHandler {
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self.orchestrator.as_ref().ok_or_else(|| {
|
||||
anyhow::anyhow!("Container orchestrator not available (dev mode required)")
|
||||
})?;
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available"))?;
|
||||
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let app_id = params
|
||||
@@ -92,7 +99,7 @@ impl RpcHandler {
|
||||
validate_app_id(app_id)?;
|
||||
|
||||
orchestrator
|
||||
.stop_container(app_id)
|
||||
.stop(app_id)
|
||||
.await
|
||||
.context("Failed to stop container")?;
|
||||
|
||||
@@ -103,9 +110,10 @@ impl RpcHandler {
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self.orchestrator.as_ref().ok_or_else(|| {
|
||||
anyhow::anyhow!("Container orchestrator not available (dev mode required)")
|
||||
})?;
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available"))?;
|
||||
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let app_id = params
|
||||
@@ -119,7 +127,7 @@ impl RpcHandler {
|
||||
.unwrap_or(false);
|
||||
|
||||
orchestrator
|
||||
.remove_container(app_id, preserve_data)
|
||||
.remove(app_id, preserve_data)
|
||||
.await
|
||||
.context("Failed to remove container")?;
|
||||
|
||||
@@ -163,9 +171,9 @@ impl RpcHandler {
|
||||
return Ok(serde_json::json!(containers));
|
||||
}
|
||||
|
||||
// Fallback: scanner hasn't run yet, query podman directly
|
||||
// Fallback: scanner hasn't run yet, query the orchestrator directly.
|
||||
if let Some(orchestrator) = &self.orchestrator {
|
||||
if let Ok(containers) = orchestrator.list_containers().await {
|
||||
if let Ok(containers) = orchestrator.list().await {
|
||||
if !containers.is_empty() {
|
||||
return Ok(serde_json::to_value(containers)?);
|
||||
}
|
||||
@@ -242,9 +250,10 @@ impl RpcHandler {
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self.orchestrator.as_ref().ok_or_else(|| {
|
||||
anyhow::anyhow!("Container orchestrator not available (dev mode required)")
|
||||
})?;
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available"))?;
|
||||
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let app_id = params
|
||||
@@ -254,7 +263,7 @@ impl RpcHandler {
|
||||
validate_app_id(app_id)?;
|
||||
|
||||
let status = orchestrator
|
||||
.get_container_status(app_id)
|
||||
.status(app_id)
|
||||
.await
|
||||
.context("Failed to get container status")?;
|
||||
|
||||
@@ -265,9 +274,10 @@ impl RpcHandler {
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self.orchestrator.as_ref().ok_or_else(|| {
|
||||
anyhow::anyhow!("Container orchestrator not available (dev mode required)")
|
||||
})?;
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available"))?;
|
||||
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let app_id = params
|
||||
@@ -278,7 +288,7 @@ impl RpcHandler {
|
||||
let lines = params.get("lines").and_then(|v| v.as_u64()).unwrap_or(100) as u32;
|
||||
|
||||
let logs = orchestrator
|
||||
.get_container_logs(app_id, lines)
|
||||
.logs(app_id, lines)
|
||||
.await
|
||||
.context("Failed to get container logs")?;
|
||||
|
||||
@@ -291,12 +301,13 @@ impl RpcHandler {
|
||||
app_id: &str,
|
||||
lines: u32,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self.orchestrator.as_ref().ok_or_else(|| {
|
||||
anyhow::anyhow!("Container orchestrator not available (dev mode required)")
|
||||
})?;
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available"))?;
|
||||
|
||||
let logs = orchestrator
|
||||
.get_container_logs(app_id, lines)
|
||||
.logs(app_id, lines)
|
||||
.await
|
||||
.context("Failed to get container logs")?;
|
||||
|
||||
@@ -307,43 +318,52 @@ impl RpcHandler {
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self.orchestrator.as_ref().ok_or_else(|| {
|
||||
anyhow::anyhow!("Container orchestrator not available (dev mode required)")
|
||||
})?;
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available"))?;
|
||||
|
||||
// If app_id is provided, get health for that app
|
||||
// If app_id is provided, get health for that app.
|
||||
if let Some(params) = params {
|
||||
if let Some(app_id) = params.get("app_id").and_then(|v| v.as_str()) {
|
||||
let health = orchestrator
|
||||
.get_health_status(app_id)
|
||||
.health(app_id)
|
||||
.await
|
||||
.context("Failed to get container health")?;
|
||||
return Ok(serde_json::json!({ app_id: health }));
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, get health for all containers
|
||||
// Otherwise, get health for all containers.
|
||||
let containers = orchestrator
|
||||
.list_containers()
|
||||
.list()
|
||||
.await
|
||||
.context("Failed to list containers")?;
|
||||
|
||||
let mut health_map = serde_json::Map::new();
|
||||
for container in containers {
|
||||
if let Some(app_id) = container.name.strip_prefix("archipelago-") {
|
||||
if let Some(app_id) = app_id.strip_suffix("-dev") {
|
||||
match orchestrator.get_health_status(app_id).await {
|
||||
Ok(health) => {
|
||||
health_map
|
||||
.insert(app_id.to_string(), serde_json::Value::String(health));
|
||||
}
|
||||
Err(_) => {
|
||||
health_map.insert(
|
||||
app_id.to_string(),
|
||||
serde_json::Value::String("unknown".to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Map the runtime container name back to the app_id the orchestrator
|
||||
// knows about. Dev orchestrator uses `archipelago-<id>-dev`; Prod
|
||||
// uses bare `<id>` (or `archy-<id>` for UIs — health() accepts the
|
||||
// app_id either way since UI_APP_IDS is centralised).
|
||||
let app_id_candidate = container
|
||||
.name
|
||||
.strip_prefix("archipelago-")
|
||||
.and_then(|s| s.strip_suffix("-dev"))
|
||||
.or_else(|| container.name.strip_prefix("archy-"))
|
||||
.unwrap_or(container.name.as_str());
|
||||
match orchestrator.health(app_id_candidate).await {
|
||||
Ok(health) => {
|
||||
health_map.insert(
|
||||
app_id_candidate.to_string(),
|
||||
serde_json::Value::String(health),
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
health_map.insert(
|
||||
app_id_candidate.to_string(),
|
||||
serde_json::Value::String("unknown".to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user