You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
709 lines
25 KiB
Rust
709 lines
25 KiB
Rust
use std::os::unix::fs::OpenOptionsExt;
|
|
use std::{io, fs, fmt, os, path, process, time};
|
|
use fmt::Display;
|
|
use os::fd::{AsFd, AsRawFd};
|
|
use os::unix::{fs::PermissionsExt, process::CommandExt};
|
|
use path::{Path, PathBuf};
|
|
use std::sync::OnceLock;
|
|
|
|
use nix::sched::{clone, CloneCb, CloneFlags, setns};
|
|
use nix::sys::{signal::{kill, Signal}, wait::{waitpid, WaitPidFlag}};
|
|
use nix::unistd::{dup2, pivot_root, setgid, setgroups, sethostname, setuid, Gid, Pid, Uid, User};
|
|
use nix::mount::{mount, MntFlags, MsFlags, umount2};
|
|
use uuid;
|
|
use toml;
|
|
use serde::{Deserialize, Serialize};
|
|
use clap::Parser;
|
|
|
|
use error::{Result, RockerError};
|
|
mod error;
|
|
use network::{create_network, remove_network};
|
|
mod network;
|
|
|
|
static WORKSPACE: &str = "/home/rocker";
|
|
static USER_NAME: &str = "rocker";
|
|
static INFO_FILE: &str = "info.toml";
|
|
static mut STACK: [u8; 1024*1024*1] = [0; 1024*1024*1];
|
|
static CLONE_FLAG: i32 = 0b1101100000000100000000000000000; // CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWNET;
|
|
static START_T: OnceLock<time::Instant> = OnceLock::new();
|
|
|
|
static LOGO: &str = r#"
|
|
____ __
|
|
/\ _`\ /\ \
|
|
\ \ \L\ \ ___ ___\ \ \/'\ __ _ __
|
|
\ \ , / / __`\ /'___\ \ , < /'__`\/\`'__\
|
|
\ \ \\ \ /\ \L\ \/\ \__/\ \ \\`\ /\ __/\ \ \/
|
|
\ \_\ \_\ \____/\ \____\\ \_\ \_\ \____\\ \_\
|
|
\/_/\/ /\/___/ \/____/ \/_/\/_/\/____/ \/_/
|
|
"#;
|
|
|
|
#[derive(Parser, Debug)]
|
|
#[command(version, about, long_about = None)]
|
|
struct RockerArgs {
|
|
// --wait/--log --run /bin/bash --image busybox
|
|
#[arg(long)]
|
|
run: Option<String>,
|
|
#[arg(long)]
|
|
image: Option<String>,
|
|
#[arg(long)]
|
|
// --volume "/tmp/test1:tmp/test1,/tmp/test2:tmp/test2"
|
|
volume: Option<String>,
|
|
#[arg(long)]
|
|
// --env "a=1,b=2,c=3"
|
|
env: Option<String>,
|
|
|
|
// --run /bin/bash --exec container_id
|
|
#[arg(long)]
|
|
exec: Option<String>,
|
|
|
|
#[arg(long)]
|
|
log: bool,
|
|
#[arg(long)]
|
|
wait: bool,
|
|
|
|
// --logs container_id
|
|
#[arg(long)]
|
|
logs: Option<String>,
|
|
|
|
// --ps
|
|
#[arg(long)]
|
|
ps: bool,
|
|
// --psa
|
|
#[arg(long)]
|
|
psa: bool,
|
|
|
|
// rm "container_id_1, container_id_2, container_id_3"
|
|
#[arg(long)]
|
|
rm: Option<String>,
|
|
|
|
// stop "container_id_1, container_id_2, container_id_3"
|
|
#[arg(long)]
|
|
stop: Option<String>,
|
|
|
|
// start container_id_1
|
|
#[arg(long)]
|
|
restart: Option<String>,
|
|
|
|
}
|
|
|
|
macro_rules! rocker_println {
|
|
($($arg:tt)*) => {
|
|
if cfg!(debug_assertions) {
|
|
println!($($arg)*);
|
|
}
|
|
};
|
|
}
|
|
|
|
/// 从images解压到volumes
|
|
fn extend_image(image_name: &String) -> Result<PathBuf> {
|
|
// 源文件
|
|
let image_path = Path::new(WORKSPACE).join("images").join(image_name);
|
|
if image_path.exists() == false {
|
|
return Err(RockerError::from(io::Error::new(io::ErrorKind::NotFound, "未找到镜像")));
|
|
}
|
|
let image_path_str = image_path.to_str().unwrap(); // 安全的unwrap
|
|
|
|
// volumes只读层
|
|
let volume_path = Path::new(WORKSPACE).join("volumes").join(image_name);
|
|
if volume_path.exists() {
|
|
return Ok(volume_path);
|
|
} else {
|
|
create_dir(&volume_path, true)?;
|
|
}
|
|
let volume_path_str = volume_path.to_str().unwrap(); // 安全的unwrap
|
|
|
|
// 解压缩
|
|
let out = process::Command::new("tar")
|
|
.arg("-xvf")
|
|
.arg(image_path_str)
|
|
.arg("-C")
|
|
.arg(volume_path_str)
|
|
.output()?;
|
|
|
|
if out.status.success() {
|
|
println!("解压缩完毕: {image_name:?}");
|
|
// 把解压缩后的文件全部给 rocker:rocker
|
|
let _ = process::Command::new("chown")
|
|
.arg("-R")
|
|
.arg("rocker:rocker")
|
|
.arg(volume_path_str)
|
|
.output()?;
|
|
Ok(volume_path)
|
|
} else {
|
|
// 删除 volume_path
|
|
std::fs::remove_dir_all(volume_path)?;
|
|
let std_err = String::from_utf8_lossy(&out.stderr);
|
|
Err(RockerError::from(io::Error::new(io::ErrorKind::Other, format!("解压缩镜像失败: {std_err}"))))
|
|
}
|
|
}
|
|
|
|
|
|
fn init_container_overlay<P: AsRef<Path>>(volume_path: P, upper_path: P, merged_path: P) -> Result<()> {
|
|
let lower_dir = volume_path.as_ref().to_string_lossy().to_string();
|
|
let upper_dir = upper_path.as_ref().to_string_lossy().to_string();
|
|
let merged_dir = merged_path.as_ref().to_string_lossy().to_string();
|
|
|
|
let dirs = format!("lowerdir={lower_dir},upperdir={upper_dir},workdir={merged_dir}");
|
|
let out = process::Command::new("mount")
|
|
.arg("-t")
|
|
.arg("overlay")
|
|
.arg("overlay")
|
|
.arg("-o")
|
|
.arg(dirs)
|
|
.arg(merged_dir)
|
|
.output()?;
|
|
|
|
if out.status.success() {
|
|
println!("容器文件系统创建完成");
|
|
} else {
|
|
let std_err = String::from_utf8_lossy(&out.stderr);
|
|
return Err(RockerError::from(io::Error::new(io::ErrorKind::Other, format!("容器文件系统创建失败: {std_err:?}"))));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn init_container_custom_volume<P: AsRef<Path>>(container_merged_path: P, custom_volume_s: &String) -> Result<()> {
|
|
for custom_volume in custom_volume_s.split(",") {
|
|
let custom_volume_v = custom_volume.split(":").collect::<Vec<&str>>();
|
|
if custom_volume_v.len() < 2 {
|
|
return Err(RockerError::OtherError(format!("volume 参数格式不正确: {custom_volume}")));
|
|
}
|
|
let host_path = custom_volume_v[0];
|
|
let container_path_buf = {
|
|
if custom_volume_v[1].starts_with("/") {
|
|
container_merged_path.as_ref().join(&custom_volume_v[1][1..])
|
|
} else {
|
|
container_merged_path.as_ref().join(&custom_volume_v[1])
|
|
}
|
|
};
|
|
let container_path = container_path_buf.to_string_lossy().to_string();
|
|
|
|
// 创建宿主机和容器内的目录
|
|
create_dir(Path::new(host_path), true)?;
|
|
create_dir(&container_path, true)?;
|
|
|
|
// 绑定
|
|
let out = process::Command::new("mount")
|
|
.arg("-o")
|
|
.arg("bind")
|
|
.arg(host_path)
|
|
.arg(container_path)
|
|
.output()?;
|
|
|
|
if out.status.success() {
|
|
println!("创建自定义 volume: {custom_volume:?}");
|
|
} else {
|
|
let std_err = String::from_utf8_lossy(&out.stderr);
|
|
return Err(RockerError::OtherError(format!("创建volume失败: {std_err}")))
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
|
|
fn init_container_env(env: &String) -> Result<()>{
|
|
for (k, _) in std::env::vars(){
|
|
std::env::remove_var(k);
|
|
}
|
|
let mut env_vec = if env.starts_with("./") || env.starts_with("/") {
|
|
// 读取出路径指定的文件作为env
|
|
let env_text = fs::read_to_string(env)?;
|
|
env_text.lines().map(String::from).collect::<Vec<String>>()
|
|
} else {
|
|
env.split(",").map(String::from).collect::<Vec<String>>()
|
|
};
|
|
|
|
env_vec.push(r#"PS1=\u\h:\W\$ "#.to_string());
|
|
|
|
for item_env in env_vec.iter() {
|
|
let item_env_v = item_env.split("=").collect::<Vec<&str>>();
|
|
if item_env_v.len() == 2 {
|
|
std::env::set_var(item_env_v[0], item_env_v[1])
|
|
} else {
|
|
if item_env.len() > 0 {
|
|
println!("env 格式不正确: {item_env}")
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
|
|
fn init_container_pivot<P: AsRef<Path>>(merged_path: P) -> Result<()> {
|
|
// 在我们没有设置 chroot之前, 需要先把所有挂载点的传播类型改为 private, 避免进程中的系统调用污染全局
|
|
mount(None::<&str>, "/", None::<&str>, MsFlags::MS_PRIVATE | MsFlags::MS_REC, None::<&str>)?;
|
|
|
|
// 修改overlayfs 为rootfs
|
|
std::env::set_current_dir(merged_path)?;
|
|
let pwd_path = std::env::current_dir()?;
|
|
let pwd_str = pwd_path.to_string_lossy().to_string();
|
|
|
|
// 挂载bind
|
|
mount(Some(pwd_str.as_str()), pwd_str.as_str(), Some("bind"), MsFlags::MS_BIND | MsFlags::MS_REC, Some(""))?;
|
|
|
|
// 创建 rootfs/.pivot_root 目录用于存储 old_root
|
|
let pivot_root_dir = format!("{pwd_str}/.pivot_root");
|
|
|
|
// 将系统rootfs切换到新的rootfs, 并设置权限
|
|
create_dir(&pivot_root_dir, true)?;
|
|
pivot_root(pwd_str.as_str(), pivot_root_dir.as_str())?;
|
|
|
|
// 修改当前进程工作目录(注意我们之前已经到rootfs内, 并且把根目录设置完毕了)
|
|
std::env::set_current_dir("/")?;
|
|
|
|
// 卸载 old_root, 并删除临时文件
|
|
umount2(".pivot_root", MntFlags::MNT_DETACH).unwrap();
|
|
std::fs::remove_dir(".pivot_root").unwrap();
|
|
Ok(())
|
|
}
|
|
|
|
|
|
fn init_container_mount() -> Result<()> {
|
|
// 挂载proc
|
|
let mount_flags = MsFlags::MS_NODEV | MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID;
|
|
mount(Some("proc"), "/proc", Some("proc"), mount_flags, Some(""))?;
|
|
|
|
// 挂载dev
|
|
mount(Some("tmpfs"), "/dev", Some("tmpfs"), mount_flags, Some("mode=755"))?;
|
|
Ok(())
|
|
}
|
|
|
|
fn init_container_log() -> Result<()> {
|
|
let log_path = Path::new("logs");
|
|
let log_file = fs::OpenOptions::new()
|
|
.append(true)
|
|
.write(true)
|
|
.read(true)
|
|
.open(log_path.join("log"))?;
|
|
let log_fd_raw = log_file.as_raw_fd();
|
|
dup2(log_fd_raw, 1)?;
|
|
dup2(log_fd_raw, 2)?;
|
|
Ok(())
|
|
}
|
|
|
|
|
|
fn init_container_user(uid: Uid, gid: Gid) -> Result<()>{
|
|
setgid(gid)?;
|
|
setgroups(&[gid])?;
|
|
setuid(uid)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn create_dir<P: AsRef<Path>>(path: P, is_any:bool) -> Result<()> {
|
|
fs::create_dir_all(&path)?;
|
|
if is_any {
|
|
fs::set_permissions(&path, PermissionsExt::from_mode(0o777))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn check_container_is_running(pid: &Pid, main_exe: &Path) -> Result<bool> {
|
|
// 检查pid对应的exe是否和外部传过来的相同
|
|
let child_exe_s= format!("/proc/{pid}/exe");
|
|
let child_exe_path = Path::new(child_exe_s.as_str());
|
|
match fs::read_link(child_exe_path) {
|
|
Ok(child_exe_path) if child_exe_path == main_exe => Ok(false),
|
|
_ => Ok(true)
|
|
}
|
|
}
|
|
|
|
fn init_exec_ns(pid: i32) -> Result<()>{
|
|
// 把当前进程加入到指定pid的namespace
|
|
for ns_name in vec!["ipc", "uts", "net", "pid", "mnt"] {
|
|
let ns_path = format!("/proc/{pid}/ns/{ns_name}");
|
|
let ns_fild = fs::File::open(ns_path)?;
|
|
setns(ns_fild.as_fd(), CloneFlags::from_bits_retain(0))? }
|
|
Ok(())
|
|
}
|
|
|
|
fn create_pause(container_root_pause_path: &Path) -> Result<()> {
|
|
fs::OpenOptions::new()
|
|
.write(true)
|
|
.create(true)
|
|
.read(true)
|
|
.open(container_root_pause_path)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn start(container_info: &ContainerInfo, cb: CloneCb, clong_flags: CloneFlags, container_merged_pause_path: &PathBuf) -> Result<i32>{
|
|
match unsafe {clone(cb, STACK.as_mut_slice(), clong_flags, None)} {
|
|
Ok(child_pid) => {
|
|
{ // 执行成功就保存吧~
|
|
let mut _container_info = container_info.clone();
|
|
_container_info.pid = child_pid.as_raw();
|
|
let _ = _container_info.flush();
|
|
}
|
|
|
|
// exec之前的步骤
|
|
create_network(&container_info.id, child_pid.as_raw());
|
|
// 删除 pause标志文件, 解开阻塞, 使其执行exec
|
|
for _ in 0..500 {
|
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
|
if std::fs::remove_file(container_merged_pause_path).is_ok() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 检查是否执行exec了
|
|
let main_exe = std::env::current_exe()?;
|
|
for _ in 0..500 {
|
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
|
if let Ok(true) = check_container_is_running(&child_pid, &main_exe) {
|
|
break;
|
|
}
|
|
}
|
|
println!("{} 启动用时: {}ms", container_info.id, START_T.get().unwrap().elapsed().as_millis());
|
|
|
|
// wait
|
|
if container_info.wait {
|
|
match waitpid(child_pid, Some(WaitPidFlag::WUNTRACED)) {
|
|
Ok(status) => {
|
|
println!("exit: {status:?}");
|
|
}
|
|
Err(e) => {
|
|
println!("{child_pid} wait err: {e}");
|
|
}
|
|
}
|
|
}
|
|
Ok(child_pid.as_raw())
|
|
}
|
|
Err(e) => {
|
|
Err(RockerError::OtherError(format!("clone err: {e}")))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn run_container(container_info: &ContainerInfo, is_exec_cmd: Option<&String>) -> Result<i32> {
|
|
// 禁止同时wait和log
|
|
if container_info.wait && container_info.log {
|
|
return Err(RockerError::OtherError("--wait/--log 禁止同时使用".to_string()));
|
|
}
|
|
|
|
let clone_flags;
|
|
|
|
let container_work_path = Path::new(WORKSPACE).join("containers").join(&container_info.id);
|
|
let container_upper_path = container_work_path.join("upper");
|
|
let container_merged_path = container_work_path.join("merged");
|
|
let container_merged_pause_path = container_work_path.join("merged").join("pause");
|
|
let container_root_pause_path = Path::new("/pause");
|
|
|
|
// 初始化容器工作目录
|
|
if create_dir(&container_work_path, true).is_ok() && create_dir(&container_upper_path, true).is_ok() && create_dir(&container_merged_path, true).is_ok() {
|
|
println!("容器工作目录创建成功");
|
|
} else {
|
|
return Err(RockerError::OtherError("容器工作目录创建失败".to_string()));
|
|
}
|
|
|
|
let rocker_user_info = User::from_name(USER_NAME)?.ok_or(RockerError::OtherError(format!("没找到 用户: {USER_NAME}")))?;
|
|
let rocker_uid = rocker_user_info.uid;
|
|
let rocker_gid = rocker_user_info.gid;
|
|
|
|
let _cb = if let Some(exec_cmd) = is_exec_cmd {
|
|
let _cb = || {
|
|
init_exec_ns(container_info.pid).unwrap();
|
|
init_container_env(&Default::default()).unwrap();
|
|
init_container_user(rocker_uid, rocker_gid).unwrap();
|
|
|
|
create_pause(container_root_pause_path).unwrap();
|
|
while container_merged_pause_path.exists() {
|
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
|
}
|
|
|
|
let cmd_vec = exec_cmd.split(" ").collect::<Vec<&str>>();
|
|
let err = process::Command::new(cmd_vec[0])
|
|
.args(&cmd_vec[1..])
|
|
.exec();
|
|
println!("🔴 execv {cmd_vec:?}失败: {err:?}");
|
|
0isize
|
|
};
|
|
clone_flags = CloneFlags::empty();
|
|
Box::new(_cb) as CloneCb
|
|
} else {
|
|
let _cb = || {
|
|
let volume_path = extend_image(&container_info.image).unwrap();
|
|
init_container_overlay(&volume_path, &container_upper_path, &container_merged_path).unwrap();
|
|
if container_info.volume.len() > 3 { // .:. 最少也要有3个字符吧
|
|
init_container_custom_volume(&container_merged_path, &container_info.volume).unwrap();
|
|
}
|
|
sethostname(USER_NAME).unwrap();
|
|
init_container_env(&container_info.env).unwrap();
|
|
init_container_pivot(&container_merged_path).unwrap();
|
|
init_container_mount().unwrap();
|
|
if container_info.log {
|
|
init_container_log().unwrap();
|
|
}
|
|
init_container_user(rocker_uid, rocker_gid).unwrap();
|
|
|
|
create_pause(&container_root_pause_path).unwrap();
|
|
while container_root_pause_path.exists() {
|
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
|
}
|
|
|
|
let cmd_vec = container_info.run.split(" ").collect::<Vec<&str>>();
|
|
let err = process::Command::new(cmd_vec[0])
|
|
.args(&cmd_vec[1..])
|
|
.exec();
|
|
println!("🔴 execv {cmd_vec:?}失败: {err:?}");
|
|
0isize
|
|
};
|
|
clone_flags = CloneFlags::from_bits_truncate(CLONE_FLAG);
|
|
Box::new(_cb) as CloneCb
|
|
};
|
|
|
|
start(container_info, _cb, clone_flags, &container_merged_pause_path)
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
|
|
enum ContainerStatus {
|
|
READY,
|
|
RUNNING,
|
|
STOP,
|
|
}
|
|
|
|
impl Display for ContainerStatus {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::READY => write!(f, "😀"),
|
|
Self::RUNNING => write!(f, "🟢"),
|
|
Self::STOP => write!(f, "🔴"),
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
struct ContainerInfo {
|
|
id: String,
|
|
pid: i32,
|
|
status: ContainerStatus,
|
|
|
|
run: String, // /bin/bash
|
|
image: String, // busybox
|
|
volume: String, // /root/tmp:/root/tmp,/root/tmp1:/root/tmp1
|
|
env: String, // a=1,b=2,c=3 或者 env文件路径
|
|
log: bool,
|
|
wait: bool,
|
|
}
|
|
|
|
impl ContainerInfo {
|
|
fn flush(&self) -> Result<()>{
|
|
let container_info_path = Path::new(WORKSPACE).join("containers").join(&self.id).join(INFO_FILE);
|
|
let toml_str = toml::to_string(&self)?;
|
|
fs::write(container_info_path, toml_str)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Display for ContainerInfo {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
let volume: String = self.volume.chars().take(20).collect();
|
|
let env: String = self.env.chars().take(20).collect();
|
|
let run: String = self.run.chars().take(20).collect();
|
|
write!(f, "\x1b[4m{:<10} {:<8} {:<10} {:<20} {:<20} {:<20} {:<10}\x1b[24m", self.id, self.pid, self.image, run, volume, env, &self.status)
|
|
}
|
|
}
|
|
|
|
fn save_container_info(args: &RockerArgs, container_id: &String, pid: i32) -> Result<()> {
|
|
let container_info_path = Path::new(WORKSPACE).join("containers").join(container_id).join(INFO_FILE);
|
|
let container_info = ContainerInfo {
|
|
id: container_id.clone(),
|
|
pid: pid,
|
|
run: args.run.as_ref().unwrap().clone(),
|
|
image: args.image.as_ref().unwrap().clone(),
|
|
volume: args.volume.clone().unwrap_or("".to_string()),
|
|
env: args.env.clone().unwrap_or("".to_string()),
|
|
status: ContainerStatus::READY,
|
|
log: args.log,
|
|
wait: args.wait,
|
|
};
|
|
let toml_str = toml::to_string(&container_info)?;
|
|
fs::write(container_info_path, toml_str)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn get_container_info(container_id: &str) -> Result<ContainerInfo> {
|
|
let container_work_path = Path::new(WORKSPACE).join("containers").join(container_id);
|
|
let container_info_path = container_work_path.join(INFO_FILE);
|
|
let info_str = fs::read_to_string(container_info_path)?;
|
|
let mut container_info: ContainerInfo = toml::from_str(&info_str)?;
|
|
|
|
// 判断是否正在运行, 进入proc/pid/判断当前进程网络中是否有下面的设备
|
|
let dev_path = Path::new("/proc").join(container_info.pid.to_string()).join("net").join("dev");
|
|
let is_running = if let Ok(dev_text) = fs::read_to_string(dev_path){
|
|
let slave_veth_name = format!("ro_{container_id}_3");
|
|
dev_text.lines().any(|l|l.starts_with(&slave_veth_name))
|
|
} else {
|
|
false
|
|
};
|
|
if is_running {
|
|
container_info.status = ContainerStatus::RUNNING;
|
|
} else {
|
|
container_info.status = ContainerStatus::STOP;
|
|
}
|
|
Ok(container_info)
|
|
}
|
|
|
|
fn get_all_container_info() -> Result<Vec<ContainerInfo>> {
|
|
let containers_path = Path::new(WORKSPACE).join("containers");
|
|
let all_containers_info = fs::read_dir(containers_path)?
|
|
.map(|res| res.map(|e| e.file_name()))
|
|
.filter_map(|p| p.ok())
|
|
.map(|f|f.to_string_lossy().to_string())
|
|
.filter_map(|s|get_container_info(s.as_str()).ok())
|
|
.collect::<Vec<ContainerInfo>>();
|
|
Ok(all_containers_info)
|
|
}
|
|
|
|
/// 读取所有容器的状态
|
|
fn show_containers(is_show_all: bool) -> Result<()> {
|
|
println!("{:<10} {:<8} {:<10} {:<20} {:<20} {:<20} {:<10}", "id", "pid", "image", "run", "volume", "env", "status");
|
|
for container_info in get_all_container_info()? {
|
|
if is_show_all{
|
|
println!("{container_info}");
|
|
} else if container_info.status == ContainerStatus::RUNNING {
|
|
println!("{container_info}");
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn stop_container(containers_id: &str, is_remove: bool) -> Result<()> {
|
|
let containers_path = Path::new(WORKSPACE).join("containers");
|
|
if containers_id == "all" {
|
|
for container_info in get_all_container_info()?{
|
|
stop_container(container_info.id.as_str(), is_remove)?
|
|
}
|
|
return Ok(())
|
|
}
|
|
for container_id in containers_id.split(" ") {
|
|
let container_work_path = containers_path.join(container_id);
|
|
let container_merged_path = container_work_path.join("merged");
|
|
if let Ok(container_info) = get_container_info(container_id) {
|
|
// 正在运行中的需要 kill
|
|
if container_info.status == ContainerStatus::RUNNING {
|
|
let _ = kill(Pid::from_raw(container_info.pid), Signal::SIGKILL);
|
|
let pid_path = Path::new("/proc").join(container_info.pid.to_string());
|
|
while pid_path.exists() {
|
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
|
}
|
|
}
|
|
// 卸载自定义挂载点
|
|
if container_info.volume != "" {
|
|
container_info.volume
|
|
.split(",")
|
|
.filter_map(|v| v.split(":").last())
|
|
.map(|v| {
|
|
if v.starts_with("/") {
|
|
container_merged_path.join(&v[1..]).to_string_lossy().to_string()
|
|
} else {
|
|
container_merged_path.join(v).to_string_lossy().to_string()
|
|
}
|
|
})
|
|
.for_each(|s| {
|
|
match umount2(s.as_str(), MntFlags::MNT_DETACH) {
|
|
Ok(_) => println!("卸载自定卷{s}"),
|
|
Err(e) => println!("卸载卷{s}失败: {e:?}"),
|
|
}
|
|
});
|
|
}
|
|
// 卸载overlayfs
|
|
match umount2(container_merged_path.to_str().unwrap(), MntFlags::MNT_DETACH) {
|
|
Ok(_) => println!("卸载overlayfs卷: {container_merged_path:?}"),
|
|
Err(e) => println!("卸载overlayfs失败: {e:?}"),
|
|
}
|
|
|
|
println!("停止容器: {container_id:?}");
|
|
// 删除容器目录
|
|
if is_remove {
|
|
match fs::remove_dir_all(container_work_path) {
|
|
Ok(_) => println!("🟢 删除容器 {container_id} 成功"),
|
|
Err(e) => println!("🔴 删除容器失败: {e:?}"),
|
|
}
|
|
}
|
|
} else {
|
|
println!("容器不存在: {container_id}, 强制删除");
|
|
// todo 需要强制删除一下目录
|
|
match fs::remove_dir_all(&container_work_path) {
|
|
Ok(_) => println!("强制删除 {container_work_path:?} 成功"),
|
|
Err(e) => println!("强制删除失败: {e:?}"),
|
|
}
|
|
}
|
|
|
|
// 容器网络删除
|
|
remove_network(container_id);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
|
|
fn main() -> Result<()>{
|
|
println!("{LOGO}");
|
|
START_T.get_or_init(|| {
|
|
time::Instant::now()
|
|
});
|
|
|
|
let mut args = RockerArgs::parse();
|
|
if args.image.is_some() || args.restart.is_some() {
|
|
let container_info = match (&args.run, &args.image, &args.restart) {
|
|
(Some(cmd), Some(image), None) => {
|
|
ContainerInfo {
|
|
id: uuid::Uuid::new_v4().to_string()[0..8].to_string(),
|
|
pid: -1,
|
|
run: cmd.clone(),
|
|
image: image.clone(),
|
|
volume: args.volume.clone().unwrap_or_default(),
|
|
env: args.env.clone().unwrap_or_default(),
|
|
status: ContainerStatus::READY,
|
|
log: args.log,
|
|
wait: args.wait
|
|
}
|
|
}
|
|
(None, None, Some(_container_id)) => {
|
|
stop_container(_container_id, false)?;
|
|
get_container_info(_container_id)?
|
|
}
|
|
_ => {
|
|
unreachable!()
|
|
}
|
|
};
|
|
|
|
match run_container(&container_info, None) {
|
|
Ok(_) => {
|
|
if container_info.wait {
|
|
println!("🟢 容器 {} 运行完毕", container_info.id);
|
|
remove_network(&container_info.id);
|
|
} else {
|
|
println!("🟢 主进程退出");
|
|
}
|
|
}
|
|
Err(e) => {
|
|
// clone 之后的错误 这里已经无法捕获了
|
|
// clone 之前出错, 清理工作目录
|
|
println!("🔴 run_container失败: {e}");
|
|
let is_remove = args.restart.is_none();
|
|
stop_container(&container_info.id, is_remove)?;
|
|
}
|
|
}
|
|
}
|
|
else if args.ps || args.psa {
|
|
// --ps
|
|
show_containers(args.psa)?
|
|
} else if let Some(containers_id) = &args.rm {
|
|
// --rm
|
|
stop_container(containers_id, true)?;
|
|
} else if let Some(containers_id) = &args.stop {
|
|
// --stop
|
|
stop_container(containers_id, false)?;
|
|
} else if let (Some(cmd), Some(container_id)) = (&args.run, &args.exec) {
|
|
// --exec
|
|
let container_info = get_container_info(container_id).unwrap();
|
|
if container_info.status == ContainerStatus::RUNNING {
|
|
run_container(&container_info, Some(cmd)).unwrap();
|
|
} else {
|
|
println!("容器{container_id}未运行");
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
} |