Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/commands/clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::io::ErrorKind;
use std::process::Command;
use tracing::{info, warn};

use crate::constants::is_foc_devnet_image;
use crate::paths::{foc_devnet_config, foc_devnet_home};

/// Remove foc-devnet state from the home directory.
Expand Down Expand Up @@ -81,7 +82,7 @@ fn docker_not_found_error() -> Box<dyn std::error::Error> {
}

fn clean_docker_images() -> Result<(), Box<dyn std::error::Error>> {
info!("Removing foc-* Docker images");
info!("Removing foc-devnet Docker images");
let output = Command::new("docker")
.args(["images", "--format", "{{.Repository}}:{{.Tag}}"])
.output()
Expand All @@ -99,7 +100,7 @@ fn clean_docker_images() -> Result<(), Box<dyn std::error::Error>> {
let mut removed_count = 0;

for line in stdout.lines() {
if line.starts_with("foc-") {
if is_foc_devnet_image(line) {
let remove_output = Command::new("docker")
.args(["rmi", line])
.output()
Expand All @@ -117,7 +118,7 @@ fn clean_docker_images() -> Result<(), Box<dyn std::error::Error>> {
if removed_count > 0 {
info!("Removed {} Docker image(s)", removed_count);
} else {
info!("No foc-* Docker images found");
info!("No foc-devnet Docker images found");
}

Ok(())
Expand Down
53 changes: 22 additions & 31 deletions src/commands/stop.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::docker::core::{container_exists, container_is_running, docker_command};
use crate::docker::delete_all_networks;
use crate::docker::{delete_all_networks, is_foc_devnet_network, list_foc_devnet_containers};
use crate::run_id::{delete_current_run_id, load_current_run_id};
use std::error::Error;
use tracing::{info, warn};
Expand Down Expand Up @@ -155,29 +155,24 @@ fn get_run_containers(run_id: &str) -> Vec<(String, &'static str)> {
]
}

/// Force kill all containers whose name starts with "foc-"
/// Force kill all foc-devnet containers, identified by exact image name. Avoids
/// touching unrelated containers that happen to share the "foc-" prefix
/// (e.g. foc-observer-*).
fn force_kill_foc_containers() -> Result<(), Box<dyn Error>> {
info!("Force killing any remaining foc* containers...");
info!("Force killing any remaining foc-devnet containers...");

// Get all containers (running and stopped) whose name starts with "foc"
let output = docker_command(&["ps", "-aq", "--filter", "name=^foc*"])?;
let stdout_str = String::from_utf8_lossy(&output.stdout);
let container_ids: Vec<&str> = stdout_str
.lines()
.filter(|line| !line.trim().is_empty())
.collect();
let containers = list_foc_devnet_containers()?;

if container_ids.is_empty() {
info!("No remaining foc* containers found");
if containers.is_empty() {
info!("No remaining foc-devnet containers found");
return Ok(());
}

info!("Found {} remaining container(s)", container_ids.len());
info!("Found {} remaining container(s)", containers.len());

for container_id in container_ids {
info!("Force removing container {}...", container_id);
let result = docker_command(&["rm", "-f", container_id]);
match result {
for c in containers {
info!("Force removing container {}...", c.name);
match docker_command(&["rm", "-f", &c.name]) {
Ok(_) => info!("Removed"),
Err(e) => warn!("Failed: {}", e),
}
Expand All @@ -187,28 +182,24 @@ fn force_kill_foc_containers() -> Result<(), Box<dyn Error>> {
Ok(())
}

/// Force remove all Docker networks starting with "foc-" or "foc_"
/// Force remove all Docker networks belonging to foc-devnet, identified by exact
/// match against the `foc_{run_id}_{lot-net|lot-m-net|cur-m-net-N}` naming scheme.
/// Listing all networks then filtering in Rust avoids edge cases with docker's
/// own filter syntax and any unrelated networks that share a `foc_` prefix.
fn force_remove_foc_networks() -> Result<(), Box<dyn Error>> {
info!("Force removing any remaining foc* networks...");

// Get all networks starting with foc- or foc_
let output = docker_command(&[
"network",
"ls",
"--filter",
"name=^foc*",
"--format",
"{{.Name}}",
])?;
info!("Force removing any remaining foc-devnet networks...");

let output = docker_command(&["network", "ls", "--format", "{{.Name}}"])?;

let stdout_str = String::from_utf8_lossy(&output.stdout);
let network_names: Vec<&str> = stdout_str
.lines()
.filter(|line| !line.trim().is_empty())
.map(str::trim)
.filter(|line| !line.is_empty() && is_foc_devnet_network(line))
.collect();

if network_names.is_empty() {
info!("No remaining foc-* networks found");
info!("No remaining foc-devnet networks found");
return Ok(());
}

Expand Down
44 changes: 44 additions & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@ pub const REQUIRED_DOCKER_IMAGES: &[&str] = &[
CURIO_DOCKER_IMAGE,
];

/// Check whether a Docker image identifier (optionally tagged, e.g. "foc-lotus:latest")
/// belongs to foc-devnet. Used to scope destructive cleanup to our own images
/// and avoid sweeping up unrelated images that happen to start with "foc-"
/// (e.g. foc-observer-*).
pub fn is_foc_devnet_image(image: &str) -> bool {
let repo = image.split(':').next().unwrap_or(image);
matches!(
repo,
LOTUS_DOCKER_IMAGE
| LOTUS_MINER_DOCKER_IMAGE
| BUILDER_DOCKER_IMAGE
| YUGABYTE_DOCKER_IMAGE
| CURIO_DOCKER_IMAGE
)
}

/// Docker container names (base - will be prefixed with foc-c-<RUN_ID>- in practice)
pub const LOTUS_CONTAINER: &str = "foc-lotus";
pub const LOTUS_MINER_CONTAINER: &str = "foc-lotus-miner";
Expand Down Expand Up @@ -102,3 +118,31 @@ pub const CURIO_LOG_LEVEL: &str = "GOLOG_LOG_LEVEL=pdp=debug";
/// File paths within containers
pub const LOTUS_BINARY_PATH: &str = "/usr/local/bin/lotus-bins/lotus";
pub const LOTUS_MINER_BINARY_PATH: &str = "/usr/local/bin/lotus-bins/lotus-miner";

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_is_foc_devnet_image_accepts_known_images() {
for image in REQUIRED_DOCKER_IMAGES {
assert!(is_foc_devnet_image(image), "{} should match", image);
assert!(
is_foc_devnet_image(&format!("{}:latest", image)),
"{}:latest should match",
image
);
}
}

#[test]
fn test_is_foc_devnet_image_rejects_unrelated() {
assert!(!is_foc_devnet_image("foc-observer-foc-observer"));
assert!(!is_foc_devnet_image("foc-observer-foc-observer:latest"));
assert!(!is_foc_devnet_image("foc-observer-ponder-mainnet"));
assert!(!is_foc_devnet_image("portainer/portainer-ce:latest"));
assert!(!is_foc_devnet_image("foc-portainer"));
assert!(!is_foc_devnet_image("nginx"));
assert!(!is_foc_devnet_image(""));
}
}
21 changes: 12 additions & 9 deletions src/docker/logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//! The log files are stored under the run-specific directory:
//! ~/.foc-devnet/run/<run_id>/logs/<container_name>.<image_name>.docker.log

use crate::constants::is_foc_devnet_image;
use crate::docker::core::{docker_command, get_container_logs};
use crate::paths::foc_devnet_run_dir;
use std::error::Error;
Expand All @@ -22,8 +23,10 @@ pub struct ContainerInfo {
pub status: String,
}

/// List all containers (running or stopped) whose image name starts with the given prefix.
pub fn list_containers_by_image_prefix(prefix: &str) -> Result<Vec<ContainerInfo>, Box<dyn Error>> {
/// List all containers (running or stopped) whose image repository is recognized as a
/// foc-devnet image. Matching is based on the repository name via `is_foc_devnet_image`
/// and does not distinguish between tags; unrelated repositories are excluded.
pub fn list_foc_devnet_containers() -> Result<Vec<ContainerInfo>, Box<dyn Error>> {
let output = docker_command(&["ps", "-a", "--format", "{{.Names}}|{{.Image}}|{{.Status}}"])?;
let stdout = String::from_utf8_lossy(&output.stdout);

Expand All @@ -34,7 +37,7 @@ pub fn list_containers_by_image_prefix(prefix: &str) -> Result<Vec<ContainerInfo
let name = parts[0].trim().to_string();
let image = parts[1].trim().to_string();
let status = parts[2].trim().to_string();
if image.starts_with(prefix) {
if is_foc_devnet_image(&image) {
result.push(ContainerInfo {
name,
image,
Expand All @@ -46,14 +49,14 @@ pub fn list_containers_by_image_prefix(prefix: &str) -> Result<Vec<ContainerInfo
Ok(result)
}

/// Persist logs for all containers whose image starts with "foc" under the run logs directory.
/// Persist logs for all foc-devnet containers under the run logs directory.
pub fn persist_foc_container_logs(run_id: &str) -> Result<(), Box<dyn Error>> {
let containers = list_containers_by_image_prefix("foc")?;
let containers = list_foc_devnet_containers()?;
let logs_dir = foc_devnet_run_dir(run_id).join("logs");
fs::create_dir_all(&logs_dir)?;

info!(
"Persisting logs for {} foc* containers to {}",
"Persisting logs for {} foc-devnet containers to {}",
containers.len(),
logs_dir.display()
);
Expand All @@ -77,9 +80,9 @@ pub fn persist_foc_container_logs(run_id: &str) -> Result<(), Box<dyn Error>> {
Ok(())
}

/// Remove all containers whose image starts with "foc" and are not running.
/// Remove all foc-devnet containers that are not running.
pub fn remove_dead_foc_containers() -> Result<(), Box<dyn Error>> {
let containers = list_containers_by_image_prefix("foc")?;
let containers = list_foc_devnet_containers()?;
let mut removed_count = 0;

for c in containers {
Expand All @@ -106,7 +109,7 @@ pub fn remove_dead_foc_containers() -> Result<(), Box<dyn Error>> {
}
}
}
info!("✓ Removed {} dead foc* containers", removed_count);
info!("✓ Removed {} dead foc-devnet containers", removed_count);
Ok(())
}

Expand Down
4 changes: 2 additions & 2 deletions src/docker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ pub use containers::{
};
pub use init::{create_volume_directories_for_images, set_volume_ownership};
pub use logs::{
list_containers_by_image_prefix, persist_foc_container_logs, remove_dead_foc_containers,
list_foc_devnet_containers, persist_foc_container_logs, remove_dead_foc_containers,
write_post_start_status_log,
};
pub use network::{
connect_container_to_network, create_all_networks, delete_all_networks,
connect_container_to_network, create_all_networks, delete_all_networks, is_foc_devnet_network,
lotus_miner_network_name, lotus_network_name, pdp_miner_network_name,
};
pub use portainer::{start_portainer, stop_portainer};
Expand Down
49 changes: 49 additions & 0 deletions src/docker/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,23 @@
use crate::constants::MAX_PDP_SP_COUNT;

use super::core::docker_command;
use regex::Regex;
use std::error::Error;
use std::sync::LazyLock;
use tracing::info;

/// Network names (suffixes)
const LOTUS_NET_SUFFIX: &str = "lot-net";
const LOTUS_MINER_NET_SUFFIX: &str = "lot-m-net";
const CURIO_MINER_NET_SUFFIX: &str = "cur-m-net";

/// Matches the exact foc-devnet network naming scheme: `foc_{run_id}_{suffix}`,
/// where suffix is one of the known network types. Used to scope cleanup so
/// unrelated docker-compose networks (e.g. `foc_observer_default`) are not touched.
static FOC_DEVNET_NETWORK_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^foc_.+_(lot-net|lot-m-net|cur-m-net-\d+)$").expect("static regex pattern")
});

/// Get the Lotus network name for a run ID
pub fn lotus_network_name(run_id: &str) -> String {
format!("foc_{}_{}", run_id, LOTUS_NET_SUFFIX)
Expand All @@ -30,6 +39,11 @@ pub fn pdp_miner_network_name(run_id: &str, sp_idx: usize) -> String {
format!("foc_{}_{}-{}", run_id, CURIO_MINER_NET_SUFFIX, sp_idx)
}

/// Check whether a Docker network name matches the foc-devnet naming scheme.
pub fn is_foc_devnet_network(name: &str) -> bool {
FOC_DEVNET_NETWORK_RE.is_match(name)
}

/// Check if a Docker network exists
pub fn network_exists(network_name: &str) -> Result<bool, Box<dyn Error>> {
let output = docker_command(&[
Expand Down Expand Up @@ -163,3 +177,38 @@ pub fn connect_container_to_network(
docker_command(&["network", "connect", network_name, container_name])?;
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_is_foc_devnet_network_matches_real_names() {
let run_id = "20260507T1747_DoofyBear";
assert!(is_foc_devnet_network(&lotus_network_name(run_id)));
assert!(is_foc_devnet_network(&lotus_miner_network_name(run_id)));
assert!(is_foc_devnet_network(&pdp_miner_network_name(run_id, 1)));
assert!(is_foc_devnet_network(&pdp_miner_network_name(run_id, 5)));
}

#[test]
fn test_is_foc_devnet_network_rejects_unrelated() {
assert!(!is_foc_devnet_network("foc-observer_default"));
assert!(!is_foc_devnet_network("foc_observer_default"));
assert!(!is_foc_devnet_network("bridge"));
assert!(!is_foc_devnet_network("foc-portainer"));
assert!(!is_foc_devnet_network(""));
}

#[test]
fn test_is_foc_devnet_network_rejects_partial_matches() {
// Must end with a known suffix, no trailing junk.
assert!(!is_foc_devnet_network("foc_run_lot-net-extra"));
// cur-m-net requires a numeric SP index.
assert!(!is_foc_devnet_network("foc_run_cur-m-net"));
assert!(!is_foc_devnet_network("foc_run_cur-m-net-"));
assert!(!is_foc_devnet_network("foc_run_cur-m-net-x"));
// Run ID portion must be non-empty.
assert!(!is_foc_devnet_network("foc__lot-net"));
}
}
Loading