fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
|
|
|
use super::package::validate_app_id;
|
fix(rpc): async container stop/start/restart; widen state mapping
RPC handlers no longer block on podman operations. container-stop on
bitcoin-core used to hold the connection for up to 600s while the UI
showed a frozen spinner; it now returns in under a second with
{status: stopping} after flipping the package state to Stopping and
broadcasting over WebSocket. Same treatment for container-start and
the new container-restart route.
Widens container-list state mapping to emit the transitional variants
(stopping, starting, restarting, installing, updating, removing,
installed, and the backup states) instead of collapsing them to
"unknown". Keeps the mapping in sync with the UI ContainerStatus.state
union so the dashboard can render the right transitional label.
Mirrors the treatment in package/runtime.rs for package.start,
package.stop, and package.restart. The body of each handler is lifted
into pure do_package_* helpers that the background task runs; state
flipping is bracketed around the spawn with revert on error. The
pre-existing post-start exit-check verification and restart stop+start
fallback run inside the spawned task, not the RPC body.
Adds container-restart route to the dispatcher. mark_user_stopped
continues to run BEFORE the spawn, preserving the ordering contract
with the crash recovery layer at runtime.rs:145-148.
2026-04-23 04:59:27 -04:00
|
|
|
use super::transitional::Op;
|
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
|
|
|
use super::RpcHandler;
|
2026-03-04 05:23:42 +00:00
|
|
|
use anyhow::{Context, Result};
|
2026-05-13 15:09:22 -04:00
|
|
|
use std::time::Duration;
|
|
|
|
|
|
|
|
|
|
const PODMAN_INSPECT_TIMEOUT: Duration = Duration::from_secs(10);
|
|
|
|
|
const PODMAN_PS_TIMEOUT: Duration = Duration::from_secs(10);
|
2026-03-04 05:23:42 +00:00
|
|
|
|
|
|
|
|
impl RpcHandler {
|
|
|
|
|
pub(super) async fn handle_container_install(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
2026-04-22 18:56:52 -04:00
|
|
|
// 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")
|
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
|
|
|
})?;
|
2026-03-04 05:23:42 +00:00
|
|
|
|
|
|
|
|
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
|
|
|
|
let manifest_path = params
|
|
|
|
|
.get("manifest_path")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Missing manifest_path"))?;
|
|
|
|
|
|
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
|
|
|
// Validate manifest path: reject traversal, resolve to canonical path
|
|
|
|
|
if manifest_path.contains("..") || manifest_path.contains('\0') {
|
2026-03-06 03:26:56 +00:00
|
|
|
return Err(anyhow::anyhow!(
|
|
|
|
|
"Invalid manifest_path: path traversal not allowed"
|
|
|
|
|
));
|
|
|
|
|
}
|
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
|
|
|
let apps_dir = self.config.data_dir.join("apps");
|
|
|
|
|
let resolved = if std::path::Path::new(manifest_path).is_absolute() {
|
|
|
|
|
std::path::PathBuf::from(manifest_path)
|
|
|
|
|
} else {
|
|
|
|
|
apps_dir.join(manifest_path)
|
|
|
|
|
};
|
|
|
|
|
let canonical = resolved
|
|
|
|
|
.canonicalize()
|
|
|
|
|
.context("Invalid manifest_path: file not found")?;
|
|
|
|
|
if !canonical.starts_with(&apps_dir) {
|
|
|
|
|
return Err(anyhow::anyhow!(
|
|
|
|
|
"Invalid manifest_path: must be under the apps directory"
|
|
|
|
|
));
|
2026-03-06 03:26:56 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-04 05:23:42 +00:00
|
|
|
// Load manifest
|
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
|
|
|
let manifest_content = tokio::fs::read_to_string(&canonical)
|
2026-03-04 05:23:42 +00:00
|
|
|
.await
|
|
|
|
|
.context("Failed to read manifest file")?;
|
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
|
|
|
let manifest: archipelago_container::AppManifest =
|
|
|
|
|
serde_yaml::from_str(&manifest_content).context("Failed to parse manifest")?;
|
2026-03-04 05:23:42 +00:00
|
|
|
|
2026-04-22 18:56:52 -04:00
|
|
|
let container_name = dev
|
2026-03-04 05:23:42 +00:00
|
|
|
.install_container(&manifest, manifest_path)
|
|
|
|
|
.await
|
|
|
|
|
.context("Failed to install container")?;
|
|
|
|
|
|
|
|
|
|
Ok(serde_json::json!(container_name))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) async fn handle_container_start(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
|
|
|
|
let app_id = params
|
|
|
|
|
.get("app_id")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Missing app_id"))?;
|
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
|
|
|
validate_app_id(app_id)?;
|
2026-03-04 05:23:42 +00:00
|
|
|
|
fix(rpc): async container stop/start/restart; widen state mapping
RPC handlers no longer block on podman operations. container-stop on
bitcoin-core used to hold the connection for up to 600s while the UI
showed a frozen spinner; it now returns in under a second with
{status: stopping} after flipping the package state to Stopping and
broadcasting over WebSocket. Same treatment for container-start and
the new container-restart route.
Widens container-list state mapping to emit the transitional variants
(stopping, starting, restarting, installing, updating, removing,
installed, and the backup states) instead of collapsing them to
"unknown". Keeps the mapping in sync with the UI ContainerStatus.state
union so the dashboard can render the right transitional label.
Mirrors the treatment in package/runtime.rs for package.start,
package.stop, and package.restart. The body of each handler is lifted
into pure do_package_* helpers that the background task runs; state
flipping is bracketed around the spawn with revert on error. The
pre-existing post-start exit-check verification and restart stop+start
fallback run inside the spawned task, not the RPC body.
Adds container-restart route to the dispatcher. mark_user_stopped
continues to run BEFORE the spawn, preserving the ordering contract
with the crash recovery layer at runtime.rs:145-148.
2026-04-23 04:59:27 -04:00
|
|
|
// User explicitly started the app — clear the user-stopped marker so
|
|
|
|
|
// crash recovery / health monitor won't second-guess it. Must happen
|
|
|
|
|
// BEFORE the spawn (see runtime.rs:145-148 for the symmetric stop
|
|
|
|
|
// side and the ordering contract crash recovery depends on).
|
|
|
|
|
crate::crash_recovery::clear_user_stopped(&self.config.data_dir, app_id).await;
|
2026-03-04 05:23:42 +00:00
|
|
|
|
fix(rpc): async container stop/start/restart; widen state mapping
RPC handlers no longer block on podman operations. container-stop on
bitcoin-core used to hold the connection for up to 600s while the UI
showed a frozen spinner; it now returns in under a second with
{status: stopping} after flipping the package state to Stopping and
broadcasting over WebSocket. Same treatment for container-start and
the new container-restart route.
Widens container-list state mapping to emit the transitional variants
(stopping, starting, restarting, installing, updating, removing,
installed, and the backup states) instead of collapsing them to
"unknown". Keeps the mapping in sync with the UI ContainerStatus.state
union so the dashboard can render the right transitional label.
Mirrors the treatment in package/runtime.rs for package.start,
package.stop, and package.restart. The body of each handler is lifted
into pure do_package_* helpers that the background task runs; state
flipping is bracketed around the spawn with revert on error. The
pre-existing post-start exit-check verification and restart stop+start
fallback run inside the spawned task, not the RPC body.
Adds container-restart route to the dispatcher. mark_user_stopped
continues to run BEFORE the spawn, preserving the ordering contract
with the crash recovery layer at runtime.rs:145-148.
2026-04-23 04:59:27 -04:00
|
|
|
// spawn_transitional returns as soon as the background task is
|
|
|
|
|
// launched (<1s). The UI sees Starting… immediately via WebSocket.
|
2026-04-28 15:00:58 -04:00
|
|
|
self.spawn_transitional(Op::Start, app_id.to_string())
|
|
|
|
|
.await?;
|
fix(rpc): async container stop/start/restart; widen state mapping
RPC handlers no longer block on podman operations. container-stop on
bitcoin-core used to hold the connection for up to 600s while the UI
showed a frozen spinner; it now returns in under a second with
{status: stopping} after flipping the package state to Stopping and
broadcasting over WebSocket. Same treatment for container-start and
the new container-restart route.
Widens container-list state mapping to emit the transitional variants
(stopping, starting, restarting, installing, updating, removing,
installed, and the backup states) instead of collapsing them to
"unknown". Keeps the mapping in sync with the UI ContainerStatus.state
union so the dashboard can render the right transitional label.
Mirrors the treatment in package/runtime.rs for package.start,
package.stop, and package.restart. The body of each handler is lifted
into pure do_package_* helpers that the background task runs; state
flipping is bracketed around the spawn with revert on error. The
pre-existing post-start exit-check verification and restart stop+start
fallback run inside the spawned task, not the RPC body.
Adds container-restart route to the dispatcher. mark_user_stopped
continues to run BEFORE the spawn, preserving the ordering contract
with the crash recovery layer at runtime.rs:145-148.
2026-04-23 04:59:27 -04:00
|
|
|
|
|
|
|
|
Ok(serde_json::json!({ "status": "starting" }))
|
2026-03-04 05:23:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) async fn handle_container_stop(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
fix(rpc): async container stop/start/restart; widen state mapping
RPC handlers no longer block on podman operations. container-stop on
bitcoin-core used to hold the connection for up to 600s while the UI
showed a frozen spinner; it now returns in under a second with
{status: stopping} after flipping the package state to Stopping and
broadcasting over WebSocket. Same treatment for container-start and
the new container-restart route.
Widens container-list state mapping to emit the transitional variants
(stopping, starting, restarting, installing, updating, removing,
installed, and the backup states) instead of collapsing them to
"unknown". Keeps the mapping in sync with the UI ContainerStatus.state
union so the dashboard can render the right transitional label.
Mirrors the treatment in package/runtime.rs for package.start,
package.stop, and package.restart. The body of each handler is lifted
into pure do_package_* helpers that the background task runs; state
flipping is bracketed around the spawn with revert on error. The
pre-existing post-start exit-check verification and restart stop+start
fallback run inside the spawned task, not the RPC body.
Adds container-restart route to the dispatcher. mark_user_stopped
continues to run BEFORE the spawn, preserving the ordering contract
with the crash recovery layer at runtime.rs:145-148.
2026-04-23 04:59:27 -04:00
|
|
|
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
|
|
|
|
let app_id = params
|
|
|
|
|
.get("app_id")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Missing app_id"))?;
|
|
|
|
|
validate_app_id(app_id)?;
|
|
|
|
|
|
|
|
|
|
// Mark as user-stopped BEFORE the spawn — ordering is load-bearing
|
|
|
|
|
// (crash recovery / health monitor inspect this flag concurrently
|
|
|
|
|
// with the in-flight stop; see runtime.rs:145-148 for the package
|
|
|
|
|
// path that also writes this in the same order).
|
|
|
|
|
crate::crash_recovery::mark_user_stopped(&self.config.data_dir, app_id).await;
|
|
|
|
|
|
|
|
|
|
// podman stop -t 600 (bitcoin-core) / -t 330 (lnd) runs in the
|
|
|
|
|
// background; the RPC returns now with "stopping".
|
2026-04-28 15:00:58 -04:00
|
|
|
self.spawn_transitional(Op::Stop, app_id.to_string())
|
|
|
|
|
.await?;
|
fix(rpc): async container stop/start/restart; widen state mapping
RPC handlers no longer block on podman operations. container-stop on
bitcoin-core used to hold the connection for up to 600s while the UI
showed a frozen spinner; it now returns in under a second with
{status: stopping} after flipping the package state to Stopping and
broadcasting over WebSocket. Same treatment for container-start and
the new container-restart route.
Widens container-list state mapping to emit the transitional variants
(stopping, starting, restarting, installing, updating, removing,
installed, and the backup states) instead of collapsing them to
"unknown". Keeps the mapping in sync with the UI ContainerStatus.state
union so the dashboard can render the right transitional label.
Mirrors the treatment in package/runtime.rs for package.start,
package.stop, and package.restart. The body of each handler is lifted
into pure do_package_* helpers that the background task runs; state
flipping is bracketed around the spawn with revert on error. The
pre-existing post-start exit-check verification and restart stop+start
fallback run inside the spawned task, not the RPC body.
Adds container-restart route to the dispatcher. mark_user_stopped
continues to run BEFORE the spawn, preserving the ordering contract
with the crash recovery layer at runtime.rs:145-148.
2026-04-23 04:59:27 -04:00
|
|
|
|
|
|
|
|
Ok(serde_json::json!({ "status": "stopping" }))
|
|
|
|
|
}
|
2026-03-04 05:23:42 +00:00
|
|
|
|
fix(rpc): async container stop/start/restart; widen state mapping
RPC handlers no longer block on podman operations. container-stop on
bitcoin-core used to hold the connection for up to 600s while the UI
showed a frozen spinner; it now returns in under a second with
{status: stopping} after flipping the package state to Stopping and
broadcasting over WebSocket. Same treatment for container-start and
the new container-restart route.
Widens container-list state mapping to emit the transitional variants
(stopping, starting, restarting, installing, updating, removing,
installed, and the backup states) instead of collapsing them to
"unknown". Keeps the mapping in sync with the UI ContainerStatus.state
union so the dashboard can render the right transitional label.
Mirrors the treatment in package/runtime.rs for package.start,
package.stop, and package.restart. The body of each handler is lifted
into pure do_package_* helpers that the background task runs; state
flipping is bracketed around the spawn with revert on error. The
pre-existing post-start exit-check verification and restart stop+start
fallback run inside the spawned task, not the RPC body.
Adds container-restart route to the dispatcher. mark_user_stopped
continues to run BEFORE the spawn, preserving the ordering contract
with the crash recovery layer at runtime.rs:145-148.
2026-04-23 04:59:27 -04:00
|
|
|
pub(super) async fn handle_container_restart(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
2026-03-04 05:23:42 +00:00
|
|
|
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
|
|
|
|
let app_id = params
|
|
|
|
|
.get("app_id")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Missing app_id"))?;
|
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
|
|
|
validate_app_id(app_id)?;
|
2026-03-04 05:23:42 +00:00
|
|
|
|
fix(rpc): async container stop/start/restart; widen state mapping
RPC handlers no longer block on podman operations. container-stop on
bitcoin-core used to hold the connection for up to 600s while the UI
showed a frozen spinner; it now returns in under a second with
{status: stopping} after flipping the package state to Stopping and
broadcasting over WebSocket. Same treatment for container-start and
the new container-restart route.
Widens container-list state mapping to emit the transitional variants
(stopping, starting, restarting, installing, updating, removing,
installed, and the backup states) instead of collapsing them to
"unknown". Keeps the mapping in sync with the UI ContainerStatus.state
union so the dashboard can render the right transitional label.
Mirrors the treatment in package/runtime.rs for package.start,
package.stop, and package.restart. The body of each handler is lifted
into pure do_package_* helpers that the background task runs; state
flipping is bracketed around the spawn with revert on error. The
pre-existing post-start exit-check verification and restart stop+start
fallback run inside the spawned task, not the RPC body.
Adds container-restart route to the dispatcher. mark_user_stopped
continues to run BEFORE the spawn, preserving the ordering contract
with the crash recovery layer at runtime.rs:145-148.
2026-04-23 04:59:27 -04:00
|
|
|
// Restart does not mark user-stopped (the user wants the app to
|
|
|
|
|
// keep running). Clear the marker as a defensive measure in case a
|
|
|
|
|
// prior stop left it set and the restart is intended to revive the
|
|
|
|
|
// normal running state.
|
|
|
|
|
crate::crash_recovery::clear_user_stopped(&self.config.data_dir, app_id).await;
|
|
|
|
|
|
|
|
|
|
self.spawn_transitional(Op::Restart, app_id.to_string())
|
|
|
|
|
.await?;
|
2026-03-04 05:23:42 +00:00
|
|
|
|
fix(rpc): async container stop/start/restart; widen state mapping
RPC handlers no longer block on podman operations. container-stop on
bitcoin-core used to hold the connection for up to 600s while the UI
showed a frozen spinner; it now returns in under a second with
{status: stopping} after flipping the package state to Stopping and
broadcasting over WebSocket. Same treatment for container-start and
the new container-restart route.
Widens container-list state mapping to emit the transitional variants
(stopping, starting, restarting, installing, updating, removing,
installed, and the backup states) instead of collapsing them to
"unknown". Keeps the mapping in sync with the UI ContainerStatus.state
union so the dashboard can render the right transitional label.
Mirrors the treatment in package/runtime.rs for package.start,
package.stop, and package.restart. The body of each handler is lifted
into pure do_package_* helpers that the background task runs; state
flipping is bracketed around the spawn with revert on error. The
pre-existing post-start exit-check verification and restart stop+start
fallback run inside the spawned task, not the RPC body.
Adds container-restart route to the dispatcher. mark_user_stopped
continues to run BEFORE the spawn, preserving the ordering contract
with the crash recovery layer at runtime.rs:145-148.
2026-04-23 04:59:27 -04:00
|
|
|
Ok(serde_json::json!({ "status": "restarting" }))
|
2026-03-04 05:23:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) async fn handle_container_remove(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
2026-04-22 18:56:52 -04:00
|
|
|
let orchestrator = self
|
|
|
|
|
.orchestrator
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available"))?;
|
2026-03-04 05:23:42 +00:00
|
|
|
|
|
|
|
|
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
|
|
|
|
let app_id = params
|
|
|
|
|
.get("app_id")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Missing app_id"))?;
|
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
|
|
|
validate_app_id(app_id)?;
|
2026-03-04 05:23:42 +00:00
|
|
|
let preserve_data = params
|
|
|
|
|
.get("preserve_data")
|
|
|
|
|
.and_then(|v| v.as_bool())
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
|
|
|
|
orchestrator
|
2026-04-22 18:56:52 -04:00
|
|
|
.remove(app_id, preserve_data)
|
2026-03-04 05:23:42 +00:00
|
|
|
.await
|
|
|
|
|
.context("Failed to remove container")?;
|
|
|
|
|
|
|
|
|
|
Ok(serde_json::json!({ "status": "removed" }))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) async fn handle_container_list(&self) -> Result<serde_json::Value> {
|
2026-04-02 01:28:11 +01:00
|
|
|
// Use the scanner's cached state for consistency with WebSocket updates.
|
|
|
|
|
// This prevents the container-list RPC from returning different results
|
|
|
|
|
// than the WebSocket-delivered package_data, which caused apps to flicker
|
|
|
|
|
// between "installed" and "not-installed" in the UI.
|
|
|
|
|
let (data, _) = self.state_manager.get_snapshot().await;
|
|
|
|
|
if data.server_info.status_info.containers_scanned && !data.package_data.is_empty() {
|
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
|
|
|
let containers: Vec<serde_json::Value> = data
|
|
|
|
|
.package_data
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(id, pkg)| {
|
fix(rpc): async container stop/start/restart; widen state mapping
RPC handlers no longer block on podman operations. container-stop on
bitcoin-core used to hold the connection for up to 600s while the UI
showed a frozen spinner; it now returns in under a second with
{status: stopping} after flipping the package state to Stopping and
broadcasting over WebSocket. Same treatment for container-start and
the new container-restart route.
Widens container-list state mapping to emit the transitional variants
(stopping, starting, restarting, installing, updating, removing,
installed, and the backup states) instead of collapsing them to
"unknown". Keeps the mapping in sync with the UI ContainerStatus.state
union so the dashboard can render the right transitional label.
Mirrors the treatment in package/runtime.rs for package.start,
package.stop, and package.restart. The body of each handler is lifted
into pure do_package_* helpers that the background task runs; state
flipping is bracketed around the spawn with revert on error. The
pre-existing post-start exit-check verification and restart stop+start
fallback run inside the spawned task, not the RPC body.
Adds container-restart route to the dispatcher. mark_user_stopped
continues to run BEFORE the spawn, preserving the ordering contract
with the crash recovery layer at runtime.rs:145-148.
2026-04-23 04:59:27 -04:00
|
|
|
// Keep this mapping in sync with the UI's
|
|
|
|
|
// ContainerStatus.state union in
|
|
|
|
|
// neode-ui/src/api/container-client.ts. The UI maps
|
|
|
|
|
// transitional variants to single-button labels
|
|
|
|
|
// (Stopping… / Starting… / Restarting…).
|
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
|
|
|
let state = match &pkg.state {
|
|
|
|
|
crate::data_model::PackageState::Running => "running",
|
|
|
|
|
crate::data_model::PackageState::Stopped => "stopped",
|
|
|
|
|
crate::data_model::PackageState::Exited => "exited",
|
fix(rpc): async container stop/start/restart; widen state mapping
RPC handlers no longer block on podman operations. container-stop on
bitcoin-core used to hold the connection for up to 600s while the UI
showed a frozen spinner; it now returns in under a second with
{status: stopping} after flipping the package state to Stopping and
broadcasting over WebSocket. Same treatment for container-start and
the new container-restart route.
Widens container-list state mapping to emit the transitional variants
(stopping, starting, restarting, installing, updating, removing,
installed, and the backup states) instead of collapsing them to
"unknown". Keeps the mapping in sync with the UI ContainerStatus.state
union so the dashboard can render the right transitional label.
Mirrors the treatment in package/runtime.rs for package.start,
package.stop, and package.restart. The body of each handler is lifted
into pure do_package_* helpers that the background task runs; state
flipping is bracketed around the spawn with revert on error. The
pre-existing post-start exit-check verification and restart stop+start
fallback run inside the spawned task, not the RPC body.
Adds container-restart route to the dispatcher. mark_user_stopped
continues to run BEFORE the spawn, preserving the ordering contract
with the crash recovery layer at runtime.rs:145-148.
2026-04-23 04:59:27 -04:00
|
|
|
crate::data_model::PackageState::Starting => "starting",
|
|
|
|
|
crate::data_model::PackageState::Stopping => "stopping",
|
|
|
|
|
crate::data_model::PackageState::Restarting => "restarting",
|
|
|
|
|
crate::data_model::PackageState::Installing => "installing",
|
|
|
|
|
crate::data_model::PackageState::Installed => "installed",
|
|
|
|
|
crate::data_model::PackageState::Updating => "updating",
|
|
|
|
|
crate::data_model::PackageState::Removing => "removing",
|
|
|
|
|
crate::data_model::PackageState::CreatingBackup => "creating-backup",
|
|
|
|
|
crate::data_model::PackageState::RestoringBackup => "restoring-backup",
|
|
|
|
|
crate::data_model::PackageState::BackingUp => "backing-up",
|
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
|
|
|
};
|
|
|
|
|
let lan = pkg
|
|
|
|
|
.installed
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|i| i.interface_addresses.get("main"))
|
|
|
|
|
.and_then(|a| a.lan_address.as_deref());
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"id": id,
|
|
|
|
|
"name": id,
|
|
|
|
|
"state": state,
|
|
|
|
|
"image": "",
|
|
|
|
|
"created": "",
|
|
|
|
|
"ports": [],
|
|
|
|
|
"lan_address": lan,
|
|
|
|
|
})
|
2026-04-02 01:28:11 +01:00
|
|
|
})
|
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
|
|
|
.collect();
|
2026-04-02 01:28:11 +01:00
|
|
|
return Ok(serde_json::json!(containers));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 18:56:52 -04:00
|
|
|
// Fallback: scanner hasn't run yet, query the orchestrator directly.
|
2026-03-04 05:23:42 +00:00
|
|
|
if let Some(orchestrator) = &self.orchestrator {
|
2026-04-22 18:56:52 -04:00
|
|
|
if let Ok(containers) = orchestrator.list().await {
|
2026-03-04 05:23:42 +00:00
|
|
|
if !containers.is_empty() {
|
|
|
|
|
return Ok(serde_json::to_value(containers)?);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: rootless podman, session hardening, boot stability, sidebar fix
Rootless podman migration (TASK-11):
- Remove sudo from all podman calls in PodmanClient + 8 backend files
- Remove sudo from all podman/docker calls in deploy script
- Restore full systemd security hardening: NoNewPrivileges,
RestrictAddressFamilies, MemoryDenyWriteExecute, RestrictRealtime,
RestrictNamespaces, RestrictSUIDSGID, SystemCallFilter, ProtectSystem=strict
- Enable loginctl linger for rootless container persistence
- Remove Ollama from auto-deploy (marketplace-only)
Session & auth hardening:
- Increase MAX_CONCURRENT_SESSIONS 20→50 (prevents eviction storms)
- Debounced 401 redirect in rpc-client.ts (prevents redirect storms)
Boot stability:
- optimize-debian.sh: adds chrony, swap, removes policy-rc.d
- deploy script: pre-restart chrony + swap setup
- ISO build: chrony package, swap file creation
- BootScreen: no longer clears localStorage (prevents splash replay)
- RootRedirect: sole owner of localStorage clearing on server ready
UI fixes:
- Sidebar opacity default changed from 0→visible (fixes missing sidebar
after page-persistence login without entrance animation)
- Console.log/error wrapped in import.meta.env.DEV guards
- Remove unused route import from RootRedirect
Beta tracking:
- CLAUDE.md: beta freeze protocol added
- MASTER_PLAN.md: TASK-11, TASK-17, phase structure
- BETA-PROGRESS.md: initial tracking doc
- Tagged v1.2.0-alpha.1 as pre-rootless baseline
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 13:53:27 +00:00
|
|
|
let output = tokio::process::Command::new("podman")
|
|
|
|
|
.args(["ps", "-a", "--format", "json"])
|
2026-03-04 05:23:42 +00:00
|
|
|
.output()
|
|
|
|
|
.await
|
|
|
|
|
.context("Failed to list containers via podman")?;
|
|
|
|
|
|
|
|
|
|
if !output.status.success() {
|
|
|
|
|
return Ok(serde_json::json!([]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
|
|
|
if stdout.trim().is_empty() {
|
|
|
|
|
return Ok(serde_json::json!([]));
|
|
|
|
|
}
|
|
|
|
|
|
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
|
|
|
let podman_containers: Vec<serde_json::Value> =
|
|
|
|
|
serde_json::from_str(&stdout).unwrap_or_else(|_| Vec::new());
|
2026-03-04 05:23:42 +00:00
|
|
|
|
|
|
|
|
let containers: Vec<serde_json::Value> = podman_containers
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|c| {
|
|
|
|
|
let state = c.get("State").and_then(|v| v.as_str()).unwrap_or("unknown");
|
|
|
|
|
let mapped_state = match state.to_lowercase().as_str() {
|
|
|
|
|
"running" => "running",
|
|
|
|
|
"exited" => "exited",
|
|
|
|
|
"stopped" => "stopped",
|
|
|
|
|
"created" => "created",
|
|
|
|
|
"paused" => "paused",
|
|
|
|
|
_ => "unknown",
|
|
|
|
|
};
|
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
|
|
|
let name = c
|
|
|
|
|
.get("Names")
|
|
|
|
|
.and_then(|v| v.as_array())
|
|
|
|
|
.and_then(|a| a.first())
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.unwrap_or("");
|
|
|
|
|
let ports: Vec<String> = c
|
|
|
|
|
.get("Ports")
|
2026-03-16 12:58:35 +00:00
|
|
|
.and_then(|v| v.as_array())
|
|
|
|
|
.map(|a| {
|
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
|
|
|
a.iter()
|
|
|
|
|
.filter_map(|p| {
|
|
|
|
|
let host = p.get("host_port").and_then(|v| v.as_u64())?;
|
|
|
|
|
let container = p.get("container_port").and_then(|v| v.as_u64())?;
|
|
|
|
|
let proto =
|
|
|
|
|
p.get("protocol").and_then(|v| v.as_str()).unwrap_or("tcp");
|
|
|
|
|
Some(format!("0.0.0.0:{}->{}/{}", host, container, proto))
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
2026-03-16 12:58:35 +00:00
|
|
|
})
|
|
|
|
|
.unwrap_or_default();
|
2026-03-04 05:23:42 +00:00
|
|
|
serde_json::json!({
|
|
|
|
|
"id": c.get("Id").and_then(|v| v.as_str()).unwrap_or(""),
|
|
|
|
|
"name": name,
|
|
|
|
|
"state": mapped_state,
|
|
|
|
|
"image": c.get("Image").and_then(|v| v.as_str()).unwrap_or(""),
|
|
|
|
|
"created": c.get("Created").and_then(|v| v.as_str()).unwrap_or(""),
|
2026-03-16 12:58:35 +00:00
|
|
|
"ports": ports,
|
2026-04-02 01:28:11 +01:00
|
|
|
"lan_address": serde_json::Value::Null,
|
2026-03-04 05:23:42 +00:00
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
Ok(serde_json::json!(containers))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) async fn handle_container_status(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
2026-04-22 18:56:52 -04:00
|
|
|
let orchestrator = self
|
|
|
|
|
.orchestrator
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available"))?;
|
2026-03-04 05:23:42 +00:00
|
|
|
|
|
|
|
|
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
|
|
|
|
let app_id = params
|
|
|
|
|
.get("app_id")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Missing app_id"))?;
|
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
|
|
|
validate_app_id(app_id)?;
|
2026-03-04 05:23:42 +00:00
|
|
|
|
2026-04-28 15:00:58 -04:00
|
|
|
let mut last_err: Option<anyhow::Error> = None;
|
|
|
|
|
for candidate in status_app_id_candidates(app_id) {
|
|
|
|
|
match orchestrator.status(&candidate).await {
|
|
|
|
|
Ok(status) => return Ok(serde_json::to_value(status)?),
|
|
|
|
|
Err(e) => last_err = Some(e),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback for alias drift: query podman directly by likely container
|
|
|
|
|
// names so status checks stay useful during migration.
|
|
|
|
|
for name in status_container_name_candidates(app_id) {
|
|
|
|
|
if let Some(v) = inspect_container_state_value(&name).await {
|
|
|
|
|
return Ok(v);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-04 05:23:42 +00:00
|
|
|
|
2026-04-28 15:00:58 -04:00
|
|
|
if let Some(e) = last_err {
|
|
|
|
|
return Err(e.context("Failed to get container status"));
|
|
|
|
|
}
|
|
|
|
|
Err(anyhow::anyhow!("Failed to get container status"))
|
2026-03-04 05:23:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) async fn handle_container_logs(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
2026-04-22 18:56:52 -04:00
|
|
|
let orchestrator = self
|
|
|
|
|
.orchestrator
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available"))?;
|
2026-03-04 05:23:42 +00:00
|
|
|
|
|
|
|
|
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
|
|
|
|
let app_id = params
|
|
|
|
|
.get("app_id")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Missing app_id"))?;
|
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
|
|
|
validate_app_id(app_id)?;
|
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
|
|
|
let lines = params.get("lines").and_then(|v| v.as_u64()).unwrap_or(100) as u32;
|
2026-03-04 05:23:42 +00:00
|
|
|
|
|
|
|
|
let logs = orchestrator
|
2026-04-22 18:56:52 -04:00
|
|
|
.logs(app_id, lines)
|
2026-03-04 05:23:42 +00:00
|
|
|
.await
|
|
|
|
|
.context("Failed to get container logs")?;
|
|
|
|
|
|
|
|
|
|
Ok(serde_json::to_value(logs)?)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Used by HTTP GET /api/container/logs (same logic as container-logs RPC).
|
|
|
|
|
pub async fn get_container_logs_value(
|
|
|
|
|
&self,
|
|
|
|
|
app_id: &str,
|
|
|
|
|
lines: u32,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
2026-04-22 18:56:52 -04:00
|
|
|
let orchestrator = self
|
|
|
|
|
.orchestrator
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available"))?;
|
2026-03-04 05:23:42 +00:00
|
|
|
|
|
|
|
|
let logs = orchestrator
|
2026-04-22 18:56:52 -04:00
|
|
|
.logs(app_id, lines)
|
2026-03-04 05:23:42 +00:00
|
|
|
.await
|
|
|
|
|
.context("Failed to get container logs")?;
|
|
|
|
|
|
|
|
|
|
Ok(serde_json::to_value(logs)?)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) async fn handle_container_health(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
2026-04-22 18:56:52 -04:00
|
|
|
let orchestrator = self
|
|
|
|
|
.orchestrator
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available"))?;
|
2026-03-04 05:23:42 +00:00
|
|
|
|
2026-04-22 18:56:52 -04:00
|
|
|
// If app_id is provided, get health for that app.
|
2026-03-04 05:23:42 +00:00
|
|
|
if let Some(params) = params {
|
|
|
|
|
if let Some(app_id) = params.get("app_id").and_then(|v| v.as_str()) {
|
2026-05-13 15:09:22 -04:00
|
|
|
if let Some(health) = self.stack_health(app_id).await? {
|
|
|
|
|
return Ok(serde_json::json!({ app_id: health }));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 11:29:18 -04:00
|
|
|
let mut last_err: Option<anyhow::Error> = None;
|
|
|
|
|
for candidate in status_app_id_candidates(app_id) {
|
|
|
|
|
match orchestrator.health(&candidate).await {
|
|
|
|
|
Ok(health) => return Ok(serde_json::json!({ app_id: health })),
|
|
|
|
|
Err(e) => last_err = Some(e),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for name in status_container_name_candidates(app_id) {
|
|
|
|
|
if let Some(health) = inspect_container_health_value(&name).await {
|
|
|
|
|
return Ok(serde_json::json!({ app_id: health }));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(e) = last_err {
|
|
|
|
|
return Err(e.context("Failed to get container health"));
|
|
|
|
|
}
|
|
|
|
|
return Err(anyhow::anyhow!("Failed to get container health"));
|
2026-03-04 05:23:42 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 18:56:52 -04:00
|
|
|
// Otherwise, get health for all containers.
|
2026-03-04 05:23:42 +00:00
|
|
|
let containers = orchestrator
|
2026-04-22 18:56:52 -04:00
|
|
|
.list()
|
2026-03-04 05:23:42 +00:00
|
|
|
.await
|
|
|
|
|
.context("Failed to list containers")?;
|
|
|
|
|
|
|
|
|
|
let mut health_map = serde_json::Map::new();
|
|
|
|
|
for container in containers {
|
2026-04-22 18:56:52 -04:00
|
|
|
// 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()),
|
|
|
|
|
);
|
2026-03-04 05:23:42 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(serde_json::Value::Object(health_map))
|
|
|
|
|
}
|
2026-05-13 15:09:22 -04:00
|
|
|
|
|
|
|
|
async fn stack_health(&self, app_id: &str) -> Result<Option<String>> {
|
|
|
|
|
let Some(members) = stack_health_members(app_id) else {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let orchestrator = self
|
|
|
|
|
.orchestrator
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available"))?;
|
|
|
|
|
|
|
|
|
|
let mut saw_starting = false;
|
|
|
|
|
let mut saw_unknown = false;
|
|
|
|
|
for member in members {
|
|
|
|
|
match member_health(orchestrator.as_ref(), member)
|
|
|
|
|
.await
|
|
|
|
|
.as_deref()
|
|
|
|
|
{
|
|
|
|
|
Ok(health) if health == "healthy" => {}
|
|
|
|
|
Ok(health) if health == "starting" => saw_starting = true,
|
|
|
|
|
Ok(health) if health == "unknown" => saw_unknown = true,
|
|
|
|
|
Ok(_) => return Ok(Some("unhealthy".to_string())),
|
|
|
|
|
Err(_) => saw_unknown = true,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if saw_unknown {
|
|
|
|
|
Ok(Some("unknown".to_string()))
|
|
|
|
|
} else if saw_starting {
|
|
|
|
|
Ok(Some("starting".to_string()))
|
|
|
|
|
} else {
|
|
|
|
|
Ok(Some("healthy".to_string()))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn member_health(
|
|
|
|
|
orchestrator: &dyn crate::container::traits::ContainerOrchestrator,
|
|
|
|
|
app_id: &str,
|
|
|
|
|
) -> Result<String> {
|
|
|
|
|
if let Ok(health) = orchestrator.health(app_id).await {
|
|
|
|
|
return Ok(health);
|
|
|
|
|
}
|
|
|
|
|
for name in status_container_name_candidates(app_id) {
|
|
|
|
|
if let Some(health) = inspect_container_health_value(&name).await {
|
|
|
|
|
return Ok(health);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok("unknown".to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn stack_health_members(app_id: &str) -> Option<&'static [&'static str]> {
|
|
|
|
|
match app_id {
|
|
|
|
|
"mempool" | "mempool-web" => {
|
|
|
|
|
Some(&["archy-mempool-db", "mempool-api", "archy-mempool-web"])
|
|
|
|
|
}
|
|
|
|
|
"btcpay-server" | "btcpayserver" | "btcpay" => {
|
|
|
|
|
Some(&["archy-btcpay-db", "archy-nbxplorer", "btcpay-server"])
|
|
|
|
|
}
|
|
|
|
|
"immich" => Some(&["immich_postgres", "immich_redis", "immich_server"]),
|
|
|
|
|
"indeedhub" => Some(&[
|
|
|
|
|
"indeedhub-postgres",
|
|
|
|
|
"indeedhub-redis",
|
|
|
|
|
"indeedhub-minio",
|
|
|
|
|
"indeedhub-relay",
|
|
|
|
|
"indeedhub-api",
|
|
|
|
|
"indeedhub-ffmpeg",
|
|
|
|
|
"indeedhub",
|
|
|
|
|
]),
|
|
|
|
|
"fedimint" => Some(&["fedimint"]),
|
|
|
|
|
_ => None,
|
|
|
|
|
}
|
2026-03-04 05:23:42 +00:00
|
|
|
}
|
2026-04-28 15:00:58 -04:00
|
|
|
|
|
|
|
|
fn status_app_id_candidates(app_id: &str) -> Vec<String> {
|
|
|
|
|
let mut out = Vec::new();
|
|
|
|
|
let mut push = |s: &str| {
|
|
|
|
|
if !out.iter().any(|e: &String| e == s) {
|
|
|
|
|
out.push(s.to_string());
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match app_id {
|
|
|
|
|
"bitcoin-knots" => {
|
|
|
|
|
push("bitcoin-knots");
|
|
|
|
|
push("bitcoin-core");
|
|
|
|
|
push("bitcoin");
|
|
|
|
|
}
|
|
|
|
|
"bitcoin-core" | "bitcoin" => {
|
|
|
|
|
push("bitcoin-core");
|
|
|
|
|
push("bitcoin-knots");
|
|
|
|
|
push("bitcoin");
|
|
|
|
|
}
|
|
|
|
|
"electrs" | "mempool-electrs" => {
|
|
|
|
|
push("electrs");
|
|
|
|
|
push("mempool-electrs");
|
|
|
|
|
push("electrumx");
|
|
|
|
|
}
|
2026-05-05 11:29:18 -04:00
|
|
|
"mempool" | "mempool-web" => {
|
|
|
|
|
push("mempool");
|
|
|
|
|
push("archy-mempool-web");
|
|
|
|
|
}
|
|
|
|
|
"immich" => {
|
|
|
|
|
push("immich");
|
|
|
|
|
push("immich_server");
|
|
|
|
|
}
|
2026-04-28 15:00:58 -04:00
|
|
|
_ => push(app_id),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn status_container_name_candidates(app_id: &str) -> Vec<String> {
|
|
|
|
|
let mut out = Vec::new();
|
|
|
|
|
let mut push = |s: &str| {
|
|
|
|
|
if !out.iter().any(|e: &String| e == s) {
|
|
|
|
|
out.push(s.to_string());
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match app_id {
|
|
|
|
|
"bitcoin-knots" | "bitcoin-core" | "bitcoin" => push("bitcoin-knots"),
|
|
|
|
|
"bitcoin-ui" => push("archy-bitcoin-ui"),
|
|
|
|
|
"lnd-ui" => push("archy-lnd-ui"),
|
|
|
|
|
"electrs-ui" => push("archy-electrs-ui"),
|
|
|
|
|
"electrs" | "mempool-electrs" => push("electrumx"),
|
2026-05-05 11:29:18 -04:00
|
|
|
"mempool" | "mempool-web" | "archy-mempool-web" => push("mempool"),
|
|
|
|
|
"immich" => push("immich_server"),
|
2026-04-28 15:00:58 -04:00
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
push(app_id);
|
|
|
|
|
if let Some(stripped) = app_id.strip_prefix("archy-") {
|
|
|
|
|
push(stripped);
|
|
|
|
|
} else {
|
|
|
|
|
push(&format!("archy-{}", app_id));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn inspect_container_state_value(name: &str) -> Option<serde_json::Value> {
|
2026-05-13 15:09:22 -04:00
|
|
|
if let Some(v) = ps_container_state_value(name).await {
|
|
|
|
|
return Some(v);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut cmd = tokio::process::Command::new("podman");
|
|
|
|
|
cmd.args([
|
|
|
|
|
"inspect",
|
|
|
|
|
name,
|
|
|
|
|
"--format",
|
|
|
|
|
"{{.State.Status}} {{.State.Running}} {{if .State.Healthcheck}}{{.State.Healthcheck.Status}}{{else}}none{{end}}",
|
|
|
|
|
]);
|
|
|
|
|
cmd.kill_on_drop(true);
|
|
|
|
|
let out = tokio::time::timeout(PODMAN_INSPECT_TIMEOUT, cmd.output())
|
2026-04-28 15:00:58 -04:00
|
|
|
.await
|
2026-05-13 15:09:22 -04:00
|
|
|
.ok()?
|
2026-04-28 15:00:58 -04:00
|
|
|
.ok()?;
|
|
|
|
|
if !out.status.success() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let line = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
|
|
|
|
if line.is_empty() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
let mut parts = line.split_whitespace();
|
|
|
|
|
let status = parts.next().unwrap_or("unknown");
|
|
|
|
|
let running = parts.next().unwrap_or("false") == "true";
|
2026-05-13 15:09:22 -04:00
|
|
|
let health = parts.next().unwrap_or("none");
|
2026-04-28 15:00:58 -04:00
|
|
|
Some(serde_json::json!({
|
|
|
|
|
"name": name,
|
|
|
|
|
"status": status,
|
|
|
|
|
"state": status,
|
|
|
|
|
"running": running,
|
2026-05-13 15:09:22 -04:00
|
|
|
"health": health,
|
2026-04-28 15:00:58 -04:00
|
|
|
}))
|
|
|
|
|
}
|
2026-05-05 11:29:18 -04:00
|
|
|
|
2026-05-13 15:09:22 -04:00
|
|
|
async fn ps_container_state_value(name: &str) -> Option<serde_json::Value> {
|
|
|
|
|
let mut cmd = tokio::process::Command::new("podman");
|
|
|
|
|
cmd.args([
|
|
|
|
|
"ps",
|
|
|
|
|
"-a",
|
|
|
|
|
"--filter",
|
|
|
|
|
&format!("name={name}"),
|
|
|
|
|
"--format",
|
|
|
|
|
"{{.Names}}|{{.Status}}",
|
|
|
|
|
]);
|
|
|
|
|
cmd.kill_on_drop(true);
|
|
|
|
|
let out = tokio::time::timeout(PODMAN_PS_TIMEOUT, cmd.output())
|
|
|
|
|
.await
|
|
|
|
|
.ok()?
|
|
|
|
|
.ok()?;
|
|
|
|
|
if !out.status.success() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
|
|
|
for line in stdout.lines() {
|
|
|
|
|
let mut parts = line.splitn(2, '|');
|
|
|
|
|
let container_name = parts.next().unwrap_or_default();
|
|
|
|
|
if container_name != name {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let status = parts.next().unwrap_or_default();
|
|
|
|
|
let state = state_from_podman_status(status);
|
|
|
|
|
let health = parse_health_from_status(status).unwrap_or("none");
|
|
|
|
|
return Some(serde_json::json!({
|
|
|
|
|
"name": name,
|
|
|
|
|
"status": state,
|
|
|
|
|
"state": state,
|
|
|
|
|
"running": state.eq_ignore_ascii_case("running"),
|
|
|
|
|
"health": health,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn state_from_podman_status(status: &str) -> &str {
|
|
|
|
|
if status.starts_with("Up ") {
|
|
|
|
|
"running"
|
|
|
|
|
} else if status.starts_with("Exited ") {
|
|
|
|
|
"exited"
|
|
|
|
|
} else if status.starts_with("Created") {
|
|
|
|
|
"created"
|
|
|
|
|
} else if status.starts_with("Stopping") {
|
|
|
|
|
"stopping"
|
|
|
|
|
} else if status.starts_with("Removing") {
|
|
|
|
|
"removing"
|
|
|
|
|
} else {
|
|
|
|
|
"unknown"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_health_from_status(status: &str) -> Option<&str> {
|
|
|
|
|
let start = status.rfind('(')?;
|
|
|
|
|
let end = status.rfind(')')?;
|
|
|
|
|
(start < end).then(|| &status[start + 1..end])
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 11:29:18 -04:00
|
|
|
async fn inspect_container_health_value(name: &str) -> Option<String> {
|
|
|
|
|
let v = inspect_container_state_value(name).await?;
|
2026-05-13 15:09:22 -04:00
|
|
|
if let Some(health) = v.get("health").and_then(|s| s.as_str()) {
|
|
|
|
|
if health != "none" {
|
|
|
|
|
return Some(health.to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-05 11:29:18 -04:00
|
|
|
match v.get("state").and_then(|s| s.as_str()).unwrap_or("unknown") {
|
|
|
|
|
"running" => Some("healthy".to_string()),
|
|
|
|
|
"created" => Some("starting".to_string()),
|
|
|
|
|
"paused" => Some("paused".to_string()),
|
2026-05-13 15:09:22 -04:00
|
|
|
"stopping" => Some("unhealthy".to_string()),
|
2026-05-05 11:29:18 -04:00
|
|
|
"exited" | "stopped" => Some("unhealthy".to_string()),
|
|
|
|
|
other => Some(format!("unknown:{other}")),
|
|
|
|
|
}
|
|
|
|
|
}
|