|
|
@ -5,6 +5,7 @@ use std::os::fd::AsRawFd;
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
use nix::libc::{self, setgid, CLONE_NEWCGROUP, MS_NODEV, MS_NOSUID};
|
|
|
|
use nix::libc::{self, setgid, CLONE_NEWCGROUP, MS_NODEV, MS_NOSUID};
|
|
|
|
use nix::sched::{clone, CloneCb, CloneFlags};
|
|
|
|
use nix::sched::{clone, CloneCb, CloneFlags};
|
|
|
|
|
|
|
|
use nix::sys::signal::{kill, Signal};
|
|
|
|
use nix::sys::wait::{wait, waitpid, waitid, WaitPidFlag};
|
|
|
|
use nix::sys::wait::{wait, waitpid, waitid, WaitPidFlag};
|
|
|
|
use nix::unistd::{chdir, chroot, dup2, execv, pivot_root, setuid, sleep, Gid, Pid, Uid, User, setgroups};
|
|
|
|
use nix::unistd::{chdir, chroot, dup2, execv, pivot_root, setuid, sleep, Gid, Pid, Uid, User, setgroups};
|
|
|
|
use nix::mount::{mount, MntFlags, MsFlags, umount2, umount};
|
|
|
|
use nix::mount::{mount, MntFlags, MsFlags, umount2, umount};
|
|
|
@ -34,6 +35,8 @@ struct RockerArgs {
|
|
|
|
run: Option<String>,
|
|
|
|
run: Option<String>,
|
|
|
|
#[arg(long)]
|
|
|
|
#[arg(long)]
|
|
|
|
image: Option<String>,
|
|
|
|
image: Option<String>,
|
|
|
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
|
|
|
volume: Option<String>,
|
|
|
|
|
|
|
|
|
|
|
|
#[arg(long)]
|
|
|
|
#[arg(long)]
|
|
|
|
log: bool,
|
|
|
|
log: bool,
|
|
|
@ -47,11 +50,18 @@ struct RockerArgs {
|
|
|
|
// --ps
|
|
|
|
// --ps
|
|
|
|
#[arg(long)]
|
|
|
|
#[arg(long)]
|
|
|
|
ps: bool,
|
|
|
|
ps: bool,
|
|
|
|
|
|
|
|
// --psa
|
|
|
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
|
|
|
psa: bool,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// rm container_id
|
|
|
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
|
|
|
rm: Option<Vec<String>>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// 从images解压到volumes
|
|
|
|
/// 从images解压到volumes
|
|
|
|
fn extend_image(image_name: &String) -> Result<(PathBuf)> {
|
|
|
|
fn extend_image(image_name: &String) -> Result<PathBuf> {
|
|
|
|
// 源文件
|
|
|
|
// 源文件
|
|
|
|
let image_path = Path::new(WORKSPACE).join("images").join(image_name);
|
|
|
|
let image_path = Path::new(WORKSPACE).join("images").join(image_name);
|
|
|
|
if image_path.exists() == false {
|
|
|
|
if image_path.exists() == false {
|
|
|
@ -64,7 +74,7 @@ fn extend_image(image_name: &String) -> Result<(PathBuf)> {
|
|
|
|
if volume_path.exists() {
|
|
|
|
if volume_path.exists() {
|
|
|
|
return Ok(volume_path);
|
|
|
|
return Ok(volume_path);
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
create_dir_and_set777(&volume_path)?;
|
|
|
|
create_dir(&volume_path, true)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let volume_path_str = volume_path.to_str().unwrap(); // 安全的unwrap
|
|
|
|
let volume_path_str = volume_path.to_str().unwrap(); // 安全的unwrap
|
|
|
|
|
|
|
|
|
|
|
@ -124,6 +134,45 @@ fn init_container_overlay<P: AsRef<Path>>(volume_path: P, upper_path: P, merged_
|
|
|
|
Ok(())
|
|
|
|
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), false)?;
|
|
|
|
|
|
|
|
create_dir(&container_path, true)?;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 绑定
|
|
|
|
|
|
|
|
let out = std::process::Command::new("mount")
|
|
|
|
|
|
|
|
.arg("-o")
|
|
|
|
|
|
|
|
.arg("bind")
|
|
|
|
|
|
|
|
.arg(host_path)
|
|
|
|
|
|
|
|
.arg(container_path)
|
|
|
|
|
|
|
|
.output()?;
|
|
|
|
|
|
|
|
let std_out = String::from_utf8_lossy(&out.stdout);
|
|
|
|
|
|
|
|
let std_err = String::from_utf8_lossy(&out.stderr);
|
|
|
|
|
|
|
|
if std_err.len() == 0 {
|
|
|
|
|
|
|
|
println!("创建自定义 volume: {custom_volume:?}");
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
return Err(RockerError::OtherError(format!("创建volume失败: {std_err}")))
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn init_container_pivot<P: AsRef<Path>>(merged_path: P) -> Result<()> {
|
|
|
|
fn init_container_pivot<P: AsRef<Path>>(merged_path: P) -> Result<()> {
|
|
|
|
// 在我们没有设置 chroot之前, 需要先把所有挂载点的传播类型改为 private, 避免进程中的系统调用污染全局
|
|
|
|
// 在我们没有设置 chroot之前, 需要先把所有挂载点的传播类型改为 private, 避免进程中的系统调用污染全局
|
|
|
|
mount(None::<&str>, "/", None::<&str>, MsFlags::MS_PRIVATE | MsFlags::MS_REC, None::<&str>)?;
|
|
|
|
mount(None::<&str>, "/", None::<&str>, MsFlags::MS_PRIVATE | MsFlags::MS_REC, None::<&str>)?;
|
|
|
@ -166,7 +215,7 @@ fn init_container_mount() -> Result<()> {
|
|
|
|
|
|
|
|
|
|
|
|
fn init_container_log(log: bool) -> Result<()> {
|
|
|
|
fn init_container_log(log: bool) -> Result<()> {
|
|
|
|
let log_path = Path::new("logs");
|
|
|
|
let log_path = Path::new("logs");
|
|
|
|
create_dir_and_set777(log_path)?;
|
|
|
|
create_dir(log_path, true)?;
|
|
|
|
let log_fd = File::create(log_path.join("log"))?;
|
|
|
|
let log_fd = File::create(log_path.join("log"))?;
|
|
|
|
if log {
|
|
|
|
if log {
|
|
|
|
unsafe {
|
|
|
|
unsafe {
|
|
|
@ -193,9 +242,11 @@ fn init_container_user(uid: Uid, gid: Gid) -> Result<()>{
|
|
|
|
Ok(())
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn create_dir_and_set777<P: AsRef<Path>>(path: P) -> Result<()> {
|
|
|
|
fn create_dir<P: AsRef<Path>>(path: P, is_any:bool) -> Result<()> {
|
|
|
|
fs::create_dir_all(&path)?;
|
|
|
|
fs::create_dir_all(&path)?;
|
|
|
|
fs::set_permissions(&path, PermissionsExt::from_mode(0o777))?;
|
|
|
|
if is_any {
|
|
|
|
|
|
|
|
fs::set_permissions(&path, PermissionsExt::from_mode(0o777))?;
|
|
|
|
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@ -243,9 +294,9 @@ fn run_container(cmd: &String, args: &RockerArgs, volume_path: &PathBuf) -> Resu
|
|
|
|
let container_work_path = Path::new(WORKSPACE).join("containers").join(&_container_id);
|
|
|
|
let container_work_path = Path::new(WORKSPACE).join("containers").join(&_container_id);
|
|
|
|
let container_upper_path = container_work_path.join("upper");
|
|
|
|
let container_upper_path = container_work_path.join("upper");
|
|
|
|
let container_merged_path = container_work_path.join("merged");
|
|
|
|
let container_merged_path = container_work_path.join("merged");
|
|
|
|
create_dir_and_set777(&container_work_path)?;
|
|
|
|
create_dir(&container_work_path, true)?;
|
|
|
|
create_dir_and_set777(&container_upper_path)?;
|
|
|
|
create_dir(&container_upper_path, true)?;
|
|
|
|
create_dir_and_set777(&container_merged_path)?;
|
|
|
|
create_dir(&container_merged_path, true)?;
|
|
|
|
|
|
|
|
|
|
|
|
let rocker_user_info = User::from_name(USER_NAME)?.ok_or(RockerError::OtherError(format!("没找到 用户: {USER_NAME}")))?;
|
|
|
|
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_uid = rocker_user_info.uid;
|
|
|
@ -254,6 +305,9 @@ fn run_container(cmd: &String, args: &RockerArgs, volume_path: &PathBuf) -> Resu
|
|
|
|
let _cb = || {
|
|
|
|
let _cb = || {
|
|
|
|
init_container_lock(&container_work_path).unwrap();
|
|
|
|
init_container_lock(&container_work_path).unwrap();
|
|
|
|
init_container_overlay(volume_path, &container_upper_path, &container_merged_path).unwrap();
|
|
|
|
init_container_overlay(volume_path, &container_upper_path, &container_merged_path).unwrap();
|
|
|
|
|
|
|
|
if let Some(custom_volume) = &args.volume {
|
|
|
|
|
|
|
|
init_container_custom_volume(&container_merged_path, custom_volume).unwrap();
|
|
|
|
|
|
|
|
}
|
|
|
|
init_container_pivot(&container_merged_path).unwrap();
|
|
|
|
init_container_pivot(&container_merged_path).unwrap();
|
|
|
|
init_container_mount().unwrap();
|
|
|
|
init_container_mount().unwrap();
|
|
|
|
init_container_log(args.log).unwrap();
|
|
|
|
init_container_log(args.log).unwrap();
|
|
|
@ -298,15 +352,34 @@ fn run_container(cmd: &String, args: &RockerArgs, volume_path: &PathBuf) -> Resu
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
|
|
|
|
|
|
|
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)]
|
|
|
|
#[derive(Deserialize, Serialize, Debug)]
|
|
|
|
struct ContainerInfo {
|
|
|
|
struct ContainerInfo {
|
|
|
|
id: String,
|
|
|
|
id: String,
|
|
|
|
pid: i32,
|
|
|
|
pid: i32,
|
|
|
|
run: String,
|
|
|
|
run: String, // /bin/bash
|
|
|
|
image: String,
|
|
|
|
image: String, // busybox
|
|
|
|
volume: String,
|
|
|
|
volume: String, // /root/tmp:/root/tmp,/root/tmp1:/root/tmp1
|
|
|
|
env: String,
|
|
|
|
env: String, // a=1,b=2,c=3 或者 env文件路径
|
|
|
|
status: String,
|
|
|
|
status: ContainerStatus,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Display for ContainerInfo {
|
|
|
|
impl Display for ContainerInfo {
|
|
|
@ -326,50 +399,91 @@ fn save_container_info(args: &RockerArgs, container_id: &String, pid: i32) -> Re
|
|
|
|
image: args.image.as_ref().unwrap().clone(),
|
|
|
|
image: args.image.as_ref().unwrap().clone(),
|
|
|
|
volume: "".to_string(),
|
|
|
|
volume: "".to_string(),
|
|
|
|
env: "".to_string(),
|
|
|
|
env: "".to_string(),
|
|
|
|
status: "".to_string(),
|
|
|
|
status: ContainerStatus::READY,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
let toml_str = toml::to_string(&container_info)?;
|
|
|
|
let toml_str = toml::to_string(&container_info)?;
|
|
|
|
fs::write(container_info_path, toml_str)?;
|
|
|
|
fs::write(container_info_path, toml_str)?;
|
|
|
|
Ok(())
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 读取所有容器的状态
|
|
|
|
fn get_container_info(container_id: &String) -> Result<ContainerInfo> {
|
|
|
|
fn show_containers() -> Result<()> {
|
|
|
|
let container_work_path = Path::new(WORKSPACE).join("containers").join(container_id);
|
|
|
|
let containers_path = Path::new(WORKSPACE).join("containers");
|
|
|
|
let container_info_path = container_work_path.join(INFO_FILE);
|
|
|
|
|
|
|
|
let lock_path = container_work_path.join(LOCK_FILE);
|
|
|
|
println!("{:<10}{:<8}{:<10}{:<20}{:<20}{:<20}{:<10}", "id", "pid", "image", "run", "volume", "env", "status");
|
|
|
|
let info_str = fs::read_to_string(container_info_path)?;
|
|
|
|
|
|
|
|
let mut container_info: ContainerInfo = toml::from_str(&info_str)?;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 判断是否正在运行, 首先得到该容器所有的fd
|
|
|
|
|
|
|
|
let proc_fd_path = Path::new("/proc").join(container_info.pid.to_string()).join("fd");
|
|
|
|
|
|
|
|
let is_running = if let Ok(fd_dir) = fs::read_dir(proc_fd_path) {
|
|
|
|
|
|
|
|
fd_dir.filter_map(|p|p.ok())
|
|
|
|
|
|
|
|
.filter_map(|f| fs::read_link(f.path()).ok())
|
|
|
|
|
|
|
|
.any(|p|p == lock_path)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
false
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
if is_running {
|
|
|
|
|
|
|
|
container_info.status = ContainerStatus::RUNNING;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
container_info.status = ContainerStatus::STOP;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(container_info)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let all_container_work_path = fs::read_dir(containers_path)?
|
|
|
|
fn get_all_container_info() -> Result<Vec<ContainerInfo>> {
|
|
|
|
.map(|res| res.map(|e| e.path()))
|
|
|
|
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())
|
|
|
|
.filter_map(|p| p.ok())
|
|
|
|
.collect::<Vec<PathBuf>>();
|
|
|
|
.map(|f|f.to_string_lossy().to_string())
|
|
|
|
|
|
|
|
.filter_map(|s|get_container_info(&s).ok())
|
|
|
|
for container_work_path in all_container_work_path.iter() {
|
|
|
|
.collect::<Vec<ContainerInfo>>();
|
|
|
|
let info_path = container_work_path.join(INFO_FILE);
|
|
|
|
Ok(all_containers_info)
|
|
|
|
let mut container_info: ContainerInfo;
|
|
|
|
}
|
|
|
|
if let Ok(info_str) = fs::read_to_string(info_path) {
|
|
|
|
|
|
|
|
container_info = toml::from_str(&info_str)?;
|
|
|
|
/// 读取所有容器的状态
|
|
|
|
} else {
|
|
|
|
fn show_containers(is_show_all: bool) -> Result<()> {
|
|
|
|
continue;
|
|
|
|
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}");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
// 判断是否正在运行, 首先得到该容器所有的fd
|
|
|
|
Ok(())
|
|
|
|
let lock_path = container_work_path.join(LOCK_FILE);
|
|
|
|
}
|
|
|
|
let proc_fd_path = Path::new("/proc").join(container_info.pid.to_string()).join("fd");
|
|
|
|
|
|
|
|
let is_running = if let Ok(fd_dir) = fs::read_dir(proc_fd_path) {
|
|
|
|
fn delete_container(containers_id: &Vec<String>) -> Result<()> {
|
|
|
|
fd_dir.filter_map(|p|p.ok())
|
|
|
|
for container_id in containers_id {
|
|
|
|
.filter_map(|f| fs::read_link(f.path()).ok())
|
|
|
|
if let Ok(container_info) = get_container_info(container_id) {
|
|
|
|
.any(|p|p == lock_path)
|
|
|
|
let container_work_path = Path::new(WORKSPACE).join("containers").join(container_id);
|
|
|
|
} else {
|
|
|
|
let container_merged_path = container_work_path.join("merged");
|
|
|
|
false
|
|
|
|
// 正在运行中的需要 kill
|
|
|
|
};
|
|
|
|
if container_info.status == ContainerStatus::RUNNING {
|
|
|
|
if is_running {
|
|
|
|
kill(Pid::from_raw(container_info.pid), Signal::SIGTERM)?;
|
|
|
|
container_info.status = "✅".to_string();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
|
|
|
|
container_info.status = "❌".to_string();
|
|
|
|
// 卸载自定义挂载点
|
|
|
|
|
|
|
|
let volumes_path = container_info.volume
|
|
|
|
|
|
|
|
.split(",")
|
|
|
|
|
|
|
|
.filter_map(|v| v.split(":").last())
|
|
|
|
|
|
|
|
.map(|v| container_merged_path.join(v).to_string_lossy().to_string())
|
|
|
|
|
|
|
|
.collect::<Vec<String>>();
|
|
|
|
|
|
|
|
for volume_path in volumes_path {
|
|
|
|
|
|
|
|
umount(volume_path.as_str())?;
|
|
|
|
|
|
|
|
println!("卸载卷: {volume_path:?}");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 卸载overlayfs
|
|
|
|
|
|
|
|
umount(container_merged_path.to_str().unwrap())?;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 删除容器目录
|
|
|
|
|
|
|
|
fs::remove_dir_all(container_work_path)?;
|
|
|
|
|
|
|
|
println!("删除容器: {container_id:?}");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
println!("{}", container_info);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@ -380,9 +494,11 @@ fn main() -> Result<()>{
|
|
|
|
if let (Some(cmd), Some(image_name)) = (&args.run, &args.image) {
|
|
|
|
if let (Some(cmd), Some(image_name)) = (&args.run, &args.image) {
|
|
|
|
let volume_path = extend_image(image_name)?;
|
|
|
|
let volume_path = extend_image(image_name)?;
|
|
|
|
let (container_id, pid) = run_container(cmd, &args, &volume_path)?;
|
|
|
|
let (container_id, pid) = run_container(cmd, &args, &volume_path)?;
|
|
|
|
save_container_info(&args, &container_id, pid)?;
|
|
|
|
save_container_info(&args, &container_id, pid)?; // todo 无论出不错, 都要保存一个信息, 后面需要删除用清理
|
|
|
|
} else if args.ps {
|
|
|
|
} else if args.ps || args.psa {
|
|
|
|
show_containers()?
|
|
|
|
show_containers(args.psa)?
|
|
|
|
|
|
|
|
} else if let Some(containers_id) = &args.rm {
|
|
|
|
|
|
|
|
delete_container(containers_id)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// exec
|
|
|
|
// exec
|
|
|
|