diff --git a/.gitignore b/.gitignore index a6b9c8ec2..fc143149e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,6 @@ youki !youki/ youki_integration_test -.vscode \ No newline at end of file +.vscode + +*~ diff --git a/Cargo.lock b/Cargo.lock index b276a2359..bf14bb9da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -668,6 +668,13 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "liboci-cli" +version = "0.0.1" +dependencies = [ + "clap", +] + [[package]] name = "libseccomp" version = "0.1.0" @@ -1328,6 +1335,7 @@ dependencies = [ "clap", "libcgroups", "libcontainer", + "liboci-cli", "log", "nix", "oci-spec 0.5.2 (git+https://github.com/containers/oci-spec-rs?rev=d6fb1e91742313cd0d0085937e2d6df5d4669720)", diff --git a/build.sh b/build.sh index 9db320822..e12f16a32 100755 --- a/build.sh +++ b/build.sh @@ -1,20 +1,17 @@ #!/bin/bash +set -e + TARGET=${TARGET-x86_64-unknown-linux-gnu} if [ "$TARGET" != "" ]; then TGT="--target $TARGET" fi VERSION=debug -if [[ "$1" == "--release" ]]; then +if [ "$1" == "--release" ]; then VERSION=release fi cargo build --verbose $TGT $1 -if [ ! -e ./youki ]; then - rm -f youki -fi -if [ ! -e ./youki_integration_test ]; then - rm -f youki_integration_test -fi + cp target/$TARGET/$VERSION/youki . cp target/$TARGET/$VERSION/integration_test ./youki_integration_test diff --git a/crates/liboci-cli/Cargo.toml b/crates/liboci-cli/Cargo.toml new file mode 100644 index 000000000..b84599160 --- /dev/null +++ b/crates/liboci-cli/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "liboci-cli" +version = "0.0.1" +authors = ["youki team"] +edition = "2021" +description = "Parse command line arguments for OCI container runtimes" +readme = "README.md" + +[dependencies.clap] +version = "3.0.0-beta.5" +default-features = false +features = ["std", "suggestions", "derive", "cargo"] diff --git a/crates/liboci-cli/README.md b/crates/liboci-cli/README.md new file mode 100644 index 000000000..404bf1ca4 --- /dev/null +++ b/crates/liboci-cli/README.md @@ -0,0 +1,26 @@ +# liboci-cli + +This is a crate to parse command line arguments for OCI container +runtimes as specified in the [OCI Runtime Command Line +Interface][https://github.com/opencontainers/runtime-tools/blob/master/docs/command-line-interface.md). + +## Implemented subcommands + +| Command | liboci-cli | CLI Specification | runc | crun | youki | +| :--------: | :--------: | :---------------: | :--: | :--: | :---: | +| create | ✅ | ✅ | ✅ | ✅ | ✅ | +| start | ✅ | ✅ | ✅ | ✅ | ✅ | +| state | ✅ | ✅ | ✅ | ✅ | ✅ | +| kill | ✅ | ✅ | ✅ | ✅ | ✅ | +| delete | ✅ | ✅ | ✅ | ✅ | ✅ | +| checkpoint | | | ✅ | ✅ | | +| events | ✅ | | ✅ | | ✅ | +| exec | ✅ | | ✅ | ✅ | ✅ | +| list | ✅ | | ✅ | ✅ | ✅ | +| pause | ✅ | | ✅ | ✅ | ✅ | +| ps | ✅ | | ✅ | ✅ | ✅ | +| restore | | | ✅ | ✅ | | +| resume | ✅ | | ✅ | ✅ | ✅ | +| run | ✅ | | ✅ | ✅ | ✅ | +| spec | ✅ | | ✅ | ✅ | ✅ | +| update | | | ✅ | ✅ | | diff --git a/crates/liboci-cli/src/create.rs b/crates/liboci-cli/src/create.rs new file mode 100644 index 000000000..88b3af224 --- /dev/null +++ b/crates/liboci-cli/src/create.rs @@ -0,0 +1,24 @@ +//! Handles the creation of a new container +use clap::Parser; +use std::path::PathBuf; + +/// Create a container +#[derive(Parser, Debug)] +pub struct Create { + /// File to write pid of the container created + // note that in the end, container is just another process + #[clap(short, long)] + pub pid_file: Option, + /// path to the bundle directory, containing config.json and root filesystem + #[clap(short, long, default_value = ".")] + pub bundle: PathBuf, + /// Unix socket (file) path , which will receive file descriptor of the writing end of the pseudoterminal + #[clap(short, long)] + pub console_socket: Option, + /// Pass N additional file descriptors to the container (stdio + $LISTEN_FDS + N in total) + #[clap(long, default_value = "0")] + pub preserve_fds: i32, + /// name of the container instance to be started + #[clap(forbid_empty_values = true, required = true)] + pub container_id: String, +} diff --git a/crates/liboci-cli/src/delete.rs b/crates/liboci-cli/src/delete.rs new file mode 100644 index 000000000..46db417ee --- /dev/null +++ b/crates/liboci-cli/src/delete.rs @@ -0,0 +1,11 @@ +use clap::Parser; + +/// Release any resources held by the container +#[derive(Parser, Debug)] +pub struct Delete { + #[clap(forbid_empty_values = true, required = true)] + pub container_id: String, + /// forces deletion of the container if it is still running (using SIGKILL) + #[clap(short, long)] + pub force: bool, +} diff --git a/crates/liboci-cli/src/events.rs b/crates/liboci-cli/src/events.rs new file mode 100644 index 000000000..9cc259050 --- /dev/null +++ b/crates/liboci-cli/src/events.rs @@ -0,0 +1,15 @@ +use clap::Parser; + +/// Show resource statistics for the container +#[derive(Parser, Debug)] +pub struct Events { + /// Sets the stats collection interval in seconds (default: 5s) + #[clap(long, default_value = "5")] + pub interval: u32, + /// Display the container stats only once + #[clap(long)] + pub stats: bool, + /// Name of the container instance + #[clap(forbid_empty_values = true, required = true)] + pub container_id: String, +} diff --git a/crates/liboci-cli/src/exec.rs b/crates/liboci-cli/src/exec.rs new file mode 100644 index 000000000..61c34f77a --- /dev/null +++ b/crates/liboci-cli/src/exec.rs @@ -0,0 +1,51 @@ +use std::error::Error; +use std::path::PathBuf; + +use clap::Parser; + +/// Execute a process within an existing container +#[derive(Parser, Debug)] +pub struct Exec { + /// Unix socket (file) path , which will receive file descriptor of the writing end of the pseudoterminal + #[clap(long)] + pub console_socket: Option, + #[clap(short, long)] + pub tty: bool, + #[clap(long)] + /// Current working directory of the container + pub cwd: Option, + #[clap(long)] + /// The file to which the pid of the container process should be written to + pub pid_file: Option, + /// Environment variables that should be set in the container + #[clap(short, long, parse(try_from_str = parse_key_val), number_of_values = 1)] + pub env: Vec<(String, String)>, + /// Prevent the process from gaining additional privileges + #[clap(long)] + pub no_new_privs: bool, + /// Path to process.json + #[clap(short, long)] + pub process: Option, + /// Detach from the container process + #[clap(short, long)] + pub detach: bool, + /// Identifier of the container + #[clap(forbid_empty_values = true, required = true)] + pub container_id: String, + /// Command that should be executed in the container + #[clap(required = false)] + pub command: Vec, +} + +fn parse_key_val(s: &str) -> Result<(T, U), Box> +where + T: std::str::FromStr, + T::Err: Error + Send + Sync + 'static, + U: std::str::FromStr, + U::Err: Error + Send + Sync + 'static, +{ + let pos = s + .find('=') + .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{}`", s))?; + Ok((s[..pos].parse()?, s[pos + 1..].parse()?)) +} diff --git a/crates/liboci-cli/src/info.rs b/crates/liboci-cli/src/info.rs new file mode 100644 index 000000000..3c809e967 --- /dev/null +++ b/crates/liboci-cli/src/info.rs @@ -0,0 +1,5 @@ +use clap::Parser; + +/// Show information about the system +#[derive(Parser, Debug)] +pub struct Info {} diff --git a/crates/liboci-cli/src/kill.rs b/crates/liboci-cli/src/kill.rs new file mode 100644 index 000000000..43b887749 --- /dev/null +++ b/crates/liboci-cli/src/kill.rs @@ -0,0 +1,9 @@ +use clap::Parser; + +/// Send the specified signal to the container +#[derive(Parser, Debug)] +pub struct Kill { + #[clap(forbid_empty_values = true, required = true)] + pub container_id: String, + pub signal: String, +} diff --git a/crates/liboci-cli/src/lib.rs b/crates/liboci-cli/src/lib.rs new file mode 100644 index 000000000..1c220bd67 --- /dev/null +++ b/crates/liboci-cli/src/lib.rs @@ -0,0 +1,79 @@ +use std::fmt::Debug; +use std::path::PathBuf; + +use clap::Parser; + +// Subcommands that are specified in https://github.com/opencontainers/runtime-tools/blob/master/docs/command-line-interface.md + +mod create; +mod delete; +mod kill; +mod start; +mod state; + +pub use {create::Create, delete::Delete, kill::Kill, start::Start, state::State}; + +// Other common subcommands that aren't specified in the document +mod events; +mod exec; +mod list; +mod pause; +mod ps; +mod resume; +mod run; +mod spec; + +pub use { + events::Events, exec::Exec, list::List, pause::Pause, ps::Ps, resume::Resume, run::Run, + spec::Spec, +}; + +// Subcommands parsed by liboci-cli, based on the [OCI +// runtime-spec](https://github.com/opencontainers/runtime-spec/blob/master/runtime.md) +// and specifically the [OCI Command Line +// Interface](https://github.com/opencontainers/runtime-tools/blob/master/docs/command-line-interface.md) +#[derive(Parser, Debug)] +pub enum StandardCmd { + Create(Create), + Start(Start), + State(State), + Kill(Kill), + Delete(Delete), +} + +// Extra subcommands not documented in the OCI Command Line Interface, +// but found in +// [runc](https://github.com/opencontainers/runc/blob/master/man/runc.8.md) +// and other runtimes. +#[derive(Parser, Debug)] +pub enum CommonCmd { + Events(Events), + Exec(Exec), + List(List), + Pause(Pause), + #[clap(setting=clap::AppSettings::AllowLeadingHyphen)] + Ps(Ps), + Resume(Resume), + Run(Run), + Spec(Spec), +} + +// The OCI Command Line Interface document doesn't define any global +// flags, but these are commonly accepted by runtimes +#[derive(Parser, Debug)] +pub struct GlobalOpts { + /// change log level to debug. + // Example in future : '--debug change log level to debug. (default: "warn")' + #[clap(long)] + pub debug: bool, + #[clap(short, long)] + pub log: Option, + #[clap(long)] + pub log_format: Option, + /// root directory to store container state + #[clap(short, long)] + pub root: Option, + /// Enable systemd cgroup manager, rather then use the cgroupfs directly. + #[clap(short, long)] + pub systemd_cgroup: bool, +} diff --git a/crates/liboci-cli/src/list.rs b/crates/liboci-cli/src/list.rs new file mode 100644 index 000000000..44a2ad2d1 --- /dev/null +++ b/crates/liboci-cli/src/list.rs @@ -0,0 +1,5 @@ +use clap::Parser; + +/// List created containers +#[derive(Parser, Debug)] +pub struct List {} diff --git a/crates/liboci-cli/src/pause.rs b/crates/liboci-cli/src/pause.rs new file mode 100644 index 000000000..8d50a7f9f --- /dev/null +++ b/crates/liboci-cli/src/pause.rs @@ -0,0 +1,8 @@ +use clap::Parser; + +/// Suspend the processes within the container +#[derive(Parser, Debug)] +pub struct Pause { + #[clap(forbid_empty_values = true, required = true)] + pub container_id: String, +} diff --git a/crates/liboci-cli/src/ps.rs b/crates/liboci-cli/src/ps.rs new file mode 100644 index 000000000..b192b21de --- /dev/null +++ b/crates/liboci-cli/src/ps.rs @@ -0,0 +1,14 @@ +use clap::{self, Parser}; + +/// Display the processes inside the container +#[derive(Parser, Debug)] +pub struct Ps { + /// format to display processes: table or json (default: "table") + #[clap(short, long, default_value = "table")] + pub format: String, + #[clap(forbid_empty_values = true, required = true)] + pub container_id: String, + /// options will be passed to the ps utility + #[clap(setting = clap::ArgSettings::Last)] + pub ps_options: Vec, +} diff --git a/crates/liboci-cli/src/resume.rs b/crates/liboci-cli/src/resume.rs new file mode 100644 index 000000000..8e9a5bba8 --- /dev/null +++ b/crates/liboci-cli/src/resume.rs @@ -0,0 +1,8 @@ +use clap::Parser; + +/// Resume the processes within the container +#[derive(Parser, Debug)] +pub struct Resume { + #[clap(forbid_empty_values = true, required = true)] + pub container_id: String, +} diff --git a/crates/liboci-cli/src/run.rs b/crates/liboci-cli/src/run.rs new file mode 100644 index 000000000..21399aa42 --- /dev/null +++ b/crates/liboci-cli/src/run.rs @@ -0,0 +1,23 @@ +use clap::Parser; +use std::path::PathBuf; + +/// Create a container and immediately start it +#[derive(Parser, Debug)] +pub struct Run { + /// File to write pid of the container created + // note that in the end, container is just another process + #[clap(short, long)] + pub pid_file: Option, + /// path to the bundle directory, containing config.json and root filesystem + #[clap(short, long, default_value = ".")] + pub bundle: PathBuf, + /// Unix socket (file) path , which will receive file descriptor of the writing end of the pseudoterminal + #[clap(short, long)] + pub console_socket: Option, + /// Pass N additional file descriptors to the container (stdio + $LISTEN_FDS + N in total) + #[clap(long, default_value = "0")] + pub preserve_fds: i32, + /// name of the container instance to be started + #[clap(forbid_empty_values = true, required = true)] + pub container_id: String, +} diff --git a/crates/liboci-cli/src/spec.rs b/crates/liboci-cli/src/spec.rs new file mode 100644 index 000000000..6f685fcdd --- /dev/null +++ b/crates/liboci-cli/src/spec.rs @@ -0,0 +1,9 @@ +use clap::Parser; + +/// Command generates a config.json +#[derive(Parser, Debug)] +pub struct Spec { + /// Generate a configuration for a rootless container + #[clap(long)] + pub rootless: bool, +} diff --git a/crates/liboci-cli/src/start.rs b/crates/liboci-cli/src/start.rs new file mode 100644 index 000000000..bb2341e92 --- /dev/null +++ b/crates/liboci-cli/src/start.rs @@ -0,0 +1,8 @@ +use clap::Parser; + +/// Start a previously created container +#[derive(Parser, Debug)] +pub struct Start { + #[clap(forbid_empty_values = true, required = true)] + pub container_id: String, +} diff --git a/crates/liboci-cli/src/state.rs b/crates/liboci-cli/src/state.rs new file mode 100644 index 000000000..1c97d8404 --- /dev/null +++ b/crates/liboci-cli/src/state.rs @@ -0,0 +1,8 @@ +use clap::Parser; + +/// Show the container state +#[derive(Parser, Debug)] +pub struct State { + #[clap(forbid_empty_values = true, required = true)] + pub container_id: String, +} diff --git a/crates/youki/Cargo.toml b/crates/youki/Cargo.toml index 450516983..2ff81c6f5 100644 --- a/crates/youki/Cargo.toml +++ b/crates/youki/Cargo.toml @@ -16,6 +16,7 @@ anyhow = "1.0" chrono = { version="0.4", features = ["serde"] } libcgroups = { path = "../libcgroups" } libcontainer = { path = "../libcontainer" } +liboci-cli = { path = "../liboci-cli" } log = {version = "0.4", features = ["std"]} nix = "0.23.0" oci-spec = { git = "https://github.com/containers/oci-spec-rs", rev = "d6fb1e91742313cd0d0085937e2d6df5d4669720" } diff --git a/crates/youki/src/commands/create.rs b/crates/youki/src/commands/create.rs index 8c2957eea..a49a2db57 100644 --- a/crates/youki/src/commands/create.rs +++ b/crates/youki/src/commands/create.rs @@ -1,48 +1,25 @@ //! Handles the creation of a new container use anyhow::Result; -use clap::Parser; use std::path::PathBuf; use libcontainer::{container::builder::ContainerBuilder, syscall::syscall::create_syscall}; - -/// Create a container -#[derive(Parser, Debug)] -pub struct Create { - /// File to write pid of the container created - // note that in the end, container is just another process - #[clap(short, long)] - pid_file: Option, - /// path to the bundle directory, containing config.json and root filesystem - #[clap(short, long, default_value = ".")] - bundle: PathBuf, - /// Unix socket (file) path , which will receive file descriptor of the writing end of the pseudoterminal - #[clap(short, long)] - console_socket: Option, - /// Pass N additional file descriptors to the container (stdio + $LISTEN_FDS + N in total) - #[clap(long, default_value = "0")] - preserve_fds: i32, - /// name of the container instance to be started - #[clap(forbid_empty_values = true, required = true)] - pub container_id: String, -} +use liboci_cli::Create; // One thing to note is that in the end, container is just another process in Linux // it has specific/different control group, namespace, using which program executing in it // can be given impression that is is running on a complete system, but on the system which // it is running, it is just another process, and has attributes such as pid, file descriptors, etc. // associated with it like any other process. -impl Create { - pub fn exec(&self, root_path: PathBuf, systemd_cgroup: bool) -> Result<()> { - let syscall = create_syscall(); - ContainerBuilder::new(self.container_id.clone(), syscall.as_ref()) - .with_pid_file(self.pid_file.as_ref()) - .with_console_socket(self.console_socket.as_ref()) - .with_root_path(root_path) - .with_preserved_fds(self.preserve_fds) - .as_init(&self.bundle) - .with_systemd(systemd_cgroup) - .build()?; +pub fn create(args: Create, root_path: PathBuf, systemd_cgroup: bool) -> Result<()> { + let syscall = create_syscall(); + ContainerBuilder::new(args.container_id.clone(), syscall.as_ref()) + .with_pid_file(args.pid_file.as_ref()) + .with_console_socket(args.console_socket.as_ref()) + .with_root_path(root_path) + .with_preserved_fds(args.preserve_fds) + .as_init(&args.bundle) + .with_systemd(systemd_cgroup) + .build()?; - Ok(()) - } + Ok(()) } diff --git a/crates/youki/src/commands/delete.rs b/crates/youki/src/commands/delete.rs index 7a53dd5ee..24eee161c 100644 --- a/crates/youki/src/commands/delete.rs +++ b/crates/youki/src/commands/delete.rs @@ -1,24 +1,13 @@ use crate::commands::load_container; use anyhow::{Context, Result}; -use clap::Parser; use std::path::PathBuf; -/// Release any resources held by the container -#[derive(Parser, Debug)] -pub struct Delete { - #[clap(forbid_empty_values = true, required = true)] - container_id: String, - /// forces deletion of the container if it is still running (using SIGKILL) - #[clap(short, long)] - force: bool, -} +use liboci_cli::Delete; -impl Delete { - pub fn exec(&self, root_path: PathBuf) -> Result<()> { - log::debug!("start deleting {}", self.container_id); - let mut container = load_container(root_path, &self.container_id)?; - container - .delete(self.force) - .with_context(|| format!("failed to delete container {}", self.container_id)) - } +pub fn delete(args: Delete, root_path: PathBuf) -> Result<()> { + log::debug!("start deleting {}", args.container_id); + let mut container = load_container(root_path, &args.container_id)?; + container + .delete(args.force) + .with_context(|| format!("failed to delete container {}", args.container_id)) } diff --git a/crates/youki/src/commands/events.rs b/crates/youki/src/commands/events.rs index b58af1986..61fbc0443 100644 --- a/crates/youki/src/commands/events.rs +++ b/crates/youki/src/commands/events.rs @@ -1,29 +1,14 @@ -use clap::Parser; use std::path::PathBuf; use anyhow::{Context, Result}; -use crate::commands::load_container; +use liboci_cli::Events; -/// Show resource statistics for the container -#[derive(Parser, Debug)] -pub struct Events { - /// Sets the stats collection interval in seconds (default: 5s) - #[clap(long, default_value = "5")] - pub interval: u32, - /// Display the container stats only once - #[clap(long)] - pub stats: bool, - /// Name of the container instance - #[clap(forbid_empty_values = true, required = true)] - pub container_id: String, -} +use crate::commands::load_container; -impl Events { - pub fn exec(&self, root_path: PathBuf) -> Result<()> { - let mut container = load_container(root_path, &self.container_id)?; - container - .events(self.interval, self.stats) - .with_context(|| format!("failed to get events from container {}", self.container_id)) - } +pub fn events(args: Events, root_path: PathBuf) -> Result<()> { + let mut container = load_container(root_path, &args.container_id)?; + container + .events(args.interval, args.stats) + .with_context(|| format!("failed to get events from container {}", args.container_id)) } diff --git a/crates/youki/src/commands/exec.rs b/crates/youki/src/commands/exec.rs index 6a4f35ada..abcfa4427 100644 --- a/crates/youki/src/commands/exec.rs +++ b/crates/youki/src/commands/exec.rs @@ -1,70 +1,21 @@ use anyhow::Result; -use clap::Parser; -use std::{error::Error, path::PathBuf}; +use std::path::PathBuf; use libcontainer::{container::builder::ContainerBuilder, syscall::syscall::create_syscall}; +use liboci_cli::Exec; -/// Execute a process within an existing container -#[derive(Parser, Debug)] -pub struct Exec { - /// Unix socket (file) path , which will receive file descriptor of the writing end of the pseudoterminal - #[clap(long)] - pub console_socket: Option, - #[clap(short, long)] - pub tty: bool, - #[clap(long)] - /// Current working directory of the container - pub cwd: Option, - #[clap(long)] - /// The file to which the pid of the container process should be written to - pub pid_file: Option, - /// Environment variables that should be set in the container - #[clap(short, long, parse(try_from_str = parse_key_val), number_of_values = 1)] - pub env: Vec<(String, String)>, - /// Prevent the process from gaining additional privileges - #[clap(long)] - pub no_new_privs: bool, - /// Path to process.json - #[clap(short, long)] - pub process: Option, - /// Detach from the container process - #[clap(short, long)] - pub detach: bool, - /// Identifier of the container - #[clap(forbid_empty_values = true, required = true)] - pub container_id: String, - /// Command that should be executed in the container - #[clap(required = false)] - pub command: Vec, -} - -impl Exec { - pub fn exec(&self, root_path: PathBuf) -> Result<()> { - let syscall = create_syscall(); - ContainerBuilder::new(self.container_id.clone(), syscall.as_ref()) - .with_root_path(root_path) - .with_console_socket(self.console_socket.as_ref()) - .with_pid_file(self.pid_file.as_ref()) - .as_tenant() - .with_cwd(self.cwd.as_ref()) - .with_env(self.env.clone().into_iter().collect()) - .with_process(self.process.as_ref()) - .with_no_new_privs(self.no_new_privs) - .with_process(self.process.as_ref()) - .with_container_args(self.command.clone()) - .build() - } -} - -fn parse_key_val(s: &str) -> Result<(T, U), Box> -where - T: std::str::FromStr, - T::Err: Error + Send + Sync + 'static, - U: std::str::FromStr, - U::Err: Error + Send + Sync + 'static, -{ - let pos = s - .find('=') - .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{}`", s))?; - Ok((s[..pos].parse()?, s[pos + 1..].parse()?)) +pub fn exec(args: Exec, root_path: PathBuf) -> Result<()> { + let syscall = create_syscall(); + ContainerBuilder::new(args.container_id.clone(), syscall.as_ref()) + .with_root_path(root_path) + .with_console_socket(args.console_socket.as_ref()) + .with_pid_file(args.pid_file.as_ref()) + .as_tenant() + .with_cwd(args.cwd.as_ref()) + .with_env(args.env.clone().into_iter().collect()) + .with_process(args.process.as_ref()) + .with_no_new_privs(args.no_new_privs) + .with_process(args.process.as_ref()) + .with_container_args(args.command.clone()) + .build() } diff --git a/crates/youki/src/commands/info.rs b/crates/youki/src/commands/info.rs index 146159853..67a93f215 100644 --- a/crates/youki/src/commands/info.rs +++ b/crates/youki/src/commands/info.rs @@ -12,17 +12,15 @@ use libcgroups::{common::CgroupSetup, v2::controller_type::ControllerType}; #[derive(Parser, Debug)] pub struct Info {} -impl Info { - pub fn exec(&self) -> Result<()> { - print_youki(); - print_kernel(); - print_os(); - print_hardware(); - print_cgroups(); - print_namespaces(); - - Ok(()) - } +pub fn info(_: Info) -> Result<()> { + print_youki(); + print_kernel(); + print_os(); + print_hardware(); + print_cgroups(); + print_namespaces(); + + Ok(()) } /// print Version of Youki diff --git a/crates/youki/src/commands/kill.rs b/crates/youki/src/commands/kill.rs index 2202619a7..cda6f559a 100644 --- a/crates/youki/src/commands/kill.rs +++ b/crates/youki/src/commands/kill.rs @@ -2,23 +2,13 @@ use std::{convert::TryInto, path::PathBuf}; use anyhow::Result; -use clap::Parser; use crate::commands::load_container; use libcontainer::signal::Signal; +use liboci_cli::Kill; -/// Send the specified signal to the container -#[derive(Parser, Debug)] -pub struct Kill { - #[clap(forbid_empty_values = true, required = true)] - container_id: String, - signal: String, -} - -impl Kill { - pub fn exec(&self, root_path: PathBuf) -> Result<()> { - let mut container = load_container(root_path, &self.container_id)?; - let signal: Signal = self.signal.as_str().try_into()?; - container.kill(signal) - } +pub fn kill(args: Kill, root_path: PathBuf) -> Result<()> { + let mut container = load_container(root_path, &args.container_id)?; + let signal: Signal = args.signal.as_str().try_into()?; + container.kill(signal) } diff --git a/crates/youki/src/commands/list.rs b/crates/youki/src/commands/list.rs index 53891c916..548a0d82d 100644 --- a/crates/youki/src/commands/list.rs +++ b/crates/youki/src/commands/list.rs @@ -6,61 +6,55 @@ use std::path::PathBuf; use anyhow::Result; use chrono::{DateTime, Local}; -use clap::Parser; use tabwriter::TabWriter; use libcontainer::container::{state::State, Container}; - -/// List created containers -#[derive(Parser, Debug)] -pub struct List {} - -impl List { - /// lists all existing containers - pub fn exec(&self, root_path: PathBuf) -> Result<()> { - let root_path = fs::canonicalize(root_path)?; - let mut content = String::new(); - // all containers' data is stored in their respective dir in root directory - // so we iterate through each and print the various info - for container_dir in fs::read_dir(root_path)? { - let container_dir = container_dir?.path(); - let state_file = State::file_path(&container_dir); - if !state_file.exists() { - continue; - } - - let container = Container::load(container_dir)?; - let pid = if let Some(pid) = container.pid() { - pid.to_string() - } else { - "".to_owned() - }; - - let user_name = container.creator().unwrap_or_default(); - - let created = if let Some(utc) = container.created() { - let local: DateTime = DateTime::from(utc); - local.to_rfc3339_opts(chrono::SecondsFormat::Secs, false) - } else { - "".to_owned() - }; - - content.push_str(&format!( - "{}\t{}\t{}\t{}\t{}\t{}\n", - container.id(), - pid, - container.status(), - container.bundle().display(), - created, - user_name.to_string_lossy() - )); +use liboci_cli::List; + +/// lists all existing containers +pub fn list(_: List, root_path: PathBuf) -> Result<()> { + let root_path = fs::canonicalize(root_path)?; + let mut content = String::new(); + // all containers' data is stored in their respective dir in root directory + // so we iterate through each and print the various info + for container_dir in fs::read_dir(root_path)? { + let container_dir = container_dir?.path(); + let state_file = State::file_path(&container_dir); + if !state_file.exists() { + continue; } - let mut tab_writer = TabWriter::new(io::stdout()); - writeln!(&mut tab_writer, "ID\tPID\tSTATUS\tBUNDLE\tCREATED\tCREATOR")?; - write!(&mut tab_writer, "{}", content)?; - tab_writer.flush()?; - - Ok(()) + let container = Container::load(container_dir)?; + let pid = if let Some(pid) = container.pid() { + pid.to_string() + } else { + "".to_owned() + }; + + let user_name = container.creator().unwrap_or_default(); + + let created = if let Some(utc) = container.created() { + let local: DateTime = DateTime::from(utc); + local.to_rfc3339_opts(chrono::SecondsFormat::Secs, false) + } else { + "".to_owned() + }; + + content.push_str(&format!( + "{}\t{}\t{}\t{}\t{}\t{}\n", + container.id(), + pid, + container.status(), + container.bundle().display(), + created, + user_name.to_string_lossy() + )); } + + let mut tab_writer = TabWriter::new(io::stdout()); + writeln!(&mut tab_writer, "ID\tPID\tSTATUS\tBUNDLE\tCREATED\tCREATOR")?; + write!(&mut tab_writer, "{}", content)?; + tab_writer.flush()?; + + Ok(()) } diff --git a/crates/youki/src/commands/pause.rs b/crates/youki/src/commands/pause.rs index 3efdd28d0..be650d12d 100644 --- a/crates/youki/src/commands/pause.rs +++ b/crates/youki/src/commands/pause.rs @@ -3,26 +3,18 @@ use crate::commands::load_container; use std::path::PathBuf; use anyhow::{Context, Result}; -use clap::Parser; -/// Suspend the processes within the container -#[derive(Parser, Debug)] -pub struct Pause { - #[clap(forbid_empty_values = true, required = true)] - pub container_id: String, -} +use liboci_cli::Pause; // Pausing a container indicates suspending all processes in given container // This uses Freezer cgroup to suspend and resume processes // For more information see : // https://man7.org/linux/man-pages/man7/cgroups.7.html // https://www.kernel.org/doc/Documentation/cgroup-v1/freezer-subsystem.txt -impl Pause { - pub fn exec(&self, root_path: PathBuf) -> Result<()> { - log::debug!("start pausing container {}", self.container_id); - let mut container = load_container(root_path, &self.container_id)?; - container - .pause() - .with_context(|| format!("failed to pause container {}", self.container_id)) - } +pub fn pause(args: Pause, root_path: PathBuf) -> Result<()> { + log::debug!("start pausing container {}", args.container_id); + let mut container = load_container(root_path, &args.container_id)?; + container + .pause() + .with_context(|| format!("failed to pause container {}", args.container_id)) } diff --git a/crates/youki/src/commands/ps.rs b/crates/youki/src/commands/ps.rs index 6c0c43192..7d73073fe 100644 --- a/crates/youki/src/commands/ps.rs +++ b/crates/youki/src/commands/ps.rs @@ -1,86 +1,72 @@ use anyhow::{bail, Context, Result}; -use clap::{self, Parser}; use libcgroups; use libcontainer::{container::Container, utils}; +use liboci_cli::Ps; use std::{path::PathBuf, process::Command}; -/// Display the processes inside the container -#[derive(Parser, Debug)] -pub struct Ps { - /// format to display processes: table or json (default: "table") - #[clap(short, long, default_value = "table")] - format: String, - #[clap(forbid_empty_values = true, required = true)] - pub container_id: String, - /// options will be passed to the ps utility - #[clap(setting = clap::ArgSettings::Last)] - ps_options: Vec, -} -impl Ps { - pub fn exec(&self, root_path: PathBuf) -> Result<()> { - let container_root = root_path.join(&self.container_id); - if !container_root.exists() { - bail!("{} doesn't exist.", self.container_id) - } - let container = Container::load(container_root)?; - if container.root.exists() { - let config_absolute_path = container.root.join("config.json"); - log::debug!("load spec from {:?}", config_absolute_path); - let spec = oci_spec::runtime::Spec::load(config_absolute_path)?; - log::debug!("spec: {:?}", spec); - let cgroups_path = utils::get_cgroup_path( - spec.linux() - .as_ref() - .context("no linux in spec")? - .cgroups_path(), - container.id(), - ); - let systemd_cgroup = container - .systemd() - .context("could not determine cgroup manager")?; - let cmanager = libcgroups::common::create_cgroup_manager( - cgroups_path, - systemd_cgroup, - container.id(), - )?; - let pids: Vec = cmanager - .get_all_pids()? - .iter() - .map(|pid| pid.as_raw()) - .collect(); +pub fn ps(args: Ps, root_path: PathBuf) -> Result<()> { + let container_root = root_path.join(&args.container_id); + if !container_root.exists() { + bail!("{} doesn't exist.", args.container_id) + } + let container = Container::load(container_root)?; + if container.root.exists() { + let config_absolute_path = container.root.join("config.json"); + log::debug!("load spec from {:?}", config_absolute_path); + let spec = oci_spec::runtime::Spec::load(config_absolute_path)?; + log::debug!("spec: {:?}", spec); + let cgroups_path = utils::get_cgroup_path( + spec.linux() + .as_ref() + .context("no linux in spec")? + .cgroups_path(), + container.id(), + ); + let systemd_cgroup = container + .systemd() + .context("could not determine cgroup manager")?; + let cmanager = libcgroups::common::create_cgroup_manager( + cgroups_path, + systemd_cgroup, + container.id(), + )?; + let pids: Vec = cmanager + .get_all_pids()? + .iter() + .map(|pid| pid.as_raw()) + .collect(); - if self.format == "json" { - println!("{}", serde_json::to_string(&pids)?); - } else if self.format == "table" { - let default_ps_options = vec![String::from("-ef")]; - let ps_options = if self.ps_options.is_empty() { - &default_ps_options - } else { - &self.ps_options - }; - let output = Command::new("ps").args(ps_options).output()?; - if !output.status.success() { - println!("{}", std::str::from_utf8(&output.stderr)?); - } else { - let lines = std::str::from_utf8(&output.stdout)?; - let lines: Vec<&str> = lines.split('\n').collect(); - let pid_index = get_pid_index(lines[0])?; - println!("{}", &lines[0]); - for line in &lines[1..] { - if line.is_empty() { - continue; - } - let fields: Vec<&str> = line.split_whitespace().collect(); - let pid: i32 = fields[pid_index].parse()?; - if pids.contains(&pid) { - println!("{}", line); - } + if args.format == "json" { + println!("{}", serde_json::to_string(&pids)?); + } else if args.format == "table" { + let default_ps_options = vec![String::from("-ef")]; + let ps_options = if args.ps_options.is_empty() { + &default_ps_options + } else { + &args.ps_options + }; + let output = Command::new("ps").args(ps_options).output()?; + if !output.status.success() { + println!("{}", std::str::from_utf8(&output.stderr)?); + } else { + let lines = std::str::from_utf8(&output.stdout)?; + let lines: Vec<&str> = lines.split('\n').collect(); + let pid_index = get_pid_index(lines[0])?; + println!("{}", &lines[0]); + for line in &lines[1..] { + if line.is_empty() { + continue; + } + let fields: Vec<&str> = line.split_whitespace().collect(); + let pid: i32 = fields[pid_index].parse()?; + if pids.contains(&pid) { + println!("{}", line); } } } } - Ok(()) } + Ok(()) } fn get_pid_index(title: &str) -> Result { diff --git a/crates/youki/src/commands/resume.rs b/crates/youki/src/commands/resume.rs index b051ceaa0..3897a253c 100644 --- a/crates/youki/src/commands/resume.rs +++ b/crates/youki/src/commands/resume.rs @@ -2,28 +2,20 @@ use std::path::PathBuf; use anyhow::{Context, Result}; -use clap::Parser; use crate::commands::load_container; -/// Resume the processes within the container -#[derive(Parser, Debug)] -pub struct Resume { - #[clap(forbid_empty_values = true, required = true)] - pub container_id: String, -} +use liboci_cli::Resume; // Resuming a container indicates resuming all processes in given container from paused state // This uses Freezer cgroup to suspend and resume processes // For more information see : // https://man7.org/linux/man-pages/man7/cgroups.7.html // https://www.kernel.org/doc/Documentation/cgroup-v1/freezer-subsystem.txt -impl Resume { - pub fn exec(&self, root_path: PathBuf) -> Result<()> { - log::debug!("start resuming container {}", self.container_id); - let mut container = load_container(root_path, &self.container_id)?; - container - .resume() - .with_context(|| format!("failed to resume container {}", self.container_id)) - } +pub fn resume(args: Resume, root_path: PathBuf) -> Result<()> { + log::debug!("start resuming container {}", args.container_id); + let mut container = load_container(root_path, &args.container_id)?; + container + .resume() + .with_context(|| format!("failed to resume container {}", args.container_id)) } diff --git a/crates/youki/src/commands/run.rs b/crates/youki/src/commands/run.rs index 814c00268..4a37580c5 100644 --- a/crates/youki/src/commands/run.rs +++ b/crates/youki/src/commands/run.rs @@ -1,44 +1,21 @@ use std::path::PathBuf; use anyhow::{Context, Result}; -use clap::Parser; use libcontainer::{container::builder::ContainerBuilder, syscall::syscall::create_syscall}; +use liboci_cli::Run; -/// Create a container and immediately start it -#[derive(Parser, Debug)] -pub struct Run { - /// File to write pid of the container created - // note that in the end, container is just another process - #[clap(short, long)] - pid_file: Option, - /// path to the bundle directory, containing config.json and root filesystem - #[clap(short, long, default_value = ".")] - bundle: PathBuf, - /// Unix socket (file) path , which will receive file descriptor of the writing end of the pseudoterminal - #[clap(short, long)] - console_socket: Option, - /// Pass N additional file descriptors to the container (stdio + $LISTEN_FDS + N in total) - #[clap(long, default_value = "0")] - preserve_fds: i32, - /// name of the container instance to be started - #[clap(forbid_empty_values = true, required = true)] - pub container_id: String, -} - -impl Run { - pub fn exec(&self, root_path: PathBuf, systemd_cgroup: bool) -> Result<()> { - let syscall = create_syscall(); - let mut container = ContainerBuilder::new(self.container_id.clone(), syscall.as_ref()) - .with_pid_file(self.pid_file.as_ref()) - .with_console_socket(self.console_socket.as_ref()) - .with_root_path(root_path) - .with_preserved_fds(self.preserve_fds) - .as_init(&self.bundle) - .with_systemd(systemd_cgroup) - .build()?; +pub fn run(args: Run, root_path: PathBuf, systemd_cgroup: bool) -> Result<()> { + let syscall = create_syscall(); + let mut container = ContainerBuilder::new(args.container_id.clone(), syscall.as_ref()) + .with_pid_file(args.pid_file.as_ref()) + .with_console_socket(args.console_socket.as_ref()) + .with_root_path(root_path) + .with_preserved_fds(args.preserve_fds) + .as_init(&args.bundle) + .with_systemd(systemd_cgroup) + .build()?; - container - .start() - .with_context(|| format!("failed to start container {}", self.container_id)) - } + container + .start() + .with_context(|| format!("failed to start container {}", args.container_id)) } diff --git a/crates/youki/src/commands/spec_json.rs b/crates/youki/src/commands/spec_json.rs index cf22a3750..af4680be4 100644 --- a/crates/youki/src/commands/spec_json.rs +++ b/crates/youki/src/commands/spec_json.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use clap::Parser; use nix; use oci_spec::runtime::Mount; use oci_spec::runtime::{ @@ -10,13 +9,6 @@ use serde_json::to_writer_pretty; use std::fs::File; use std::path::Path; use std::path::PathBuf; -/// Command generates a config.json -#[derive(Parser, Debug)] -pub struct SpecJson { - /// Generate a configuration for a rootless container - #[clap(long)] - pub rootless: bool, -} pub fn get_default() -> Result { Ok(Spec::default()) @@ -89,18 +81,16 @@ pub fn get_rootless() -> Result { } /// spec Cli command -impl SpecJson { - pub fn exec(&self) -> Result<()> { - let spec = if self.rootless { - get_rootless()? - } else { - get_default()? - }; +pub fn spec(args: liboci_cli::Spec) -> Result<()> { + let spec = if args.rootless { + get_rootless()? + } else { + get_default()? + }; - // write data to config.json - to_writer_pretty(&File::create("config.json")?, &spec)?; - Ok(()) - } + // write data to config.json + to_writer_pretty(&File::create("config.json")?, &spec)?; + Ok(()) } #[cfg(test)] diff --git a/crates/youki/src/commands/start.rs b/crates/youki/src/commands/start.rs index 21164f6a2..80e57a3b4 100644 --- a/crates/youki/src/commands/start.rs +++ b/crates/youki/src/commands/start.rs @@ -3,22 +3,14 @@ use std::path::PathBuf; use anyhow::{Context, Result}; -use clap::Parser; use crate::commands::load_container; -/// Start a previously created container -#[derive(Parser, Debug)] -pub struct Start { - #[clap(forbid_empty_values = true, required = true)] - pub container_id: String, -} +use liboci_cli::Start; -impl Start { - pub fn exec(&self, root_path: PathBuf) -> Result<()> { - let mut container = load_container(root_path, &self.container_id)?; - container - .start() - .with_context(|| format!("failed to start container {}", self.container_id)) - } +pub fn start(args: Start, root_path: PathBuf) -> Result<()> { + let mut container = load_container(root_path, &args.container_id)?; + container + .start() + .with_context(|| format!("failed to start container {}", args.container_id)) } diff --git a/crates/youki/src/commands/state.rs b/crates/youki/src/commands/state.rs index eda4c94e3..b136dfa19 100644 --- a/crates/youki/src/commands/state.rs +++ b/crates/youki/src/commands/state.rs @@ -2,23 +2,14 @@ use std::fs; use std::path::PathBuf; use anyhow::Result; -use clap::Parser; use libcontainer::container::Container; - -/// Show the container state -#[derive(Parser, Debug)] -pub struct State { - #[clap(forbid_empty_values = true, required = true)] - pub container_id: String, -} - -impl State { - pub fn exec(&self, root_path: PathBuf) -> Result<()> { - let root_path = fs::canonicalize(root_path)?; - let container_root = root_path.join(&self.container_id); - let container = Container::load(container_root)?; - println!("{}", serde_json::to_string_pretty(&container.state)?); - std::process::exit(0); - } +use liboci_cli::State; + +pub fn state(args: State, root_path: PathBuf) -> Result<()> { + let root_path = fs::canonicalize(root_path)?; + let container_root = root_path.join(&args.container_id); + let container = Container::load(container_root)?; + println!("{}", serde_json::to_string_pretty(&container.state)?); + std::process::exit(0); } diff --git a/crates/youki/src/main.rs b/crates/youki/src/main.rs index f1ea9fd67..2d929cbaa 100644 --- a/crates/youki/src/main.rs +++ b/crates/youki/src/main.rs @@ -12,47 +12,24 @@ use anyhow::Context; use anyhow::Result; use clap::{crate_version, Parser}; -use crate::commands::create; -use crate::commands::delete; -use crate::commands::events; -use crate::commands::exec; use crate::commands::info; -use crate::commands::kill; -use crate::commands::list; -use crate::commands::pause; -use crate::commands::ps; -use crate::commands::resume; -use crate::commands::run; -use crate::commands::spec_json; -use crate::commands::start; -use crate::commands::state; use libcontainer::rootless::rootless_required; use libcontainer::utils; use libcontainer::utils::create_dir_all_with_mode; use nix::sys::stat::Mode; use nix::unistd::getuid; +use liboci_cli::{CommonCmd, GlobalOpts, StandardCmd}; + // High-level commandline option definition // This takes global options as well as individual commands as specified in [OCI runtime-spec](https://github.com/opencontainers/runtime-spec/blob/master/runtime.md) // Also check [runc commandline documentation](https://github.com/opencontainers/runc/blob/master/man/runc.8.md) for more explanation #[derive(Parser, Debug)] #[clap(version = crate_version!(), author = "youki team")] struct Opts { - /// change log level to debug. - // Example in future : '--debug change log level to debug. (default: "warn")' - #[clap(long)] - debug: bool, - #[clap(short, long)] - log: Option, - #[clap(long)] - log_format: Option, - /// root directory to store container state - #[clap(short, long)] - root: Option, - /// Enable systemd cgroup manager, rather then use the cgroupfs directly. - #[clap(short, long)] - systemd_cgroup: bool, - /// command to actually manage container + #[clap(flatten)] + global: GlobalOpts, + #[clap(subcommand)] subcmd: SubCommand, } @@ -61,34 +38,14 @@ struct Opts { // Also for a short information, check [runc commandline documentation](https://github.com/opencontainers/runc/blob/master/man/runc.8.md) #[derive(Parser, Debug)] enum SubCommand { - #[clap(version = crate_version!(), author = "youki team")] - Create(create::Create), - #[clap(version = crate_version!(), author = "youki team")] - Start(start::Start), - #[clap(version = crate_version!(), author = "youki team")] - Run(run::Run), - #[clap(version = crate_version!(), author = "youki team")] - Exec(exec::Exec), - #[clap(version = crate_version!(), author = "youki team")] - Kill(kill::Kill), - #[clap(version = crate_version!(), author = "youki team")] - Delete(delete::Delete), - #[clap(version = crate_version!(), author = "youki team")] - State(state::State), - #[clap(version = crate_version!(), author = "youki team")] + // Standard and common commands handled by the liboci_cli crate + #[clap(flatten)] + Standard(liboci_cli::StandardCmd), + #[clap(flatten)] + Common(liboci_cli::CommonCmd), + + // Youki specific extensions Info(info::Info), - #[clap(version = crate_version!(), author = "youki team")] - Spec(spec_json::SpecJson), - #[clap(version = crate_version!(), author = "youki team")] - List(list::List), - #[clap(version = crate_version!(), author = "youki team")] - Pause(pause::Pause), - #[clap(version = crate_version!(), author = "youki team")] - Resume(resume::Resume), - #[clap(version = crate_version!(), author = "youki team")] - Events(events::Events), - #[clap(version = crate_version!(), author = "youki team", setting=clap::AppSettings::AllowLeadingHyphen)] - Ps(ps::Ps), } /// This is the entry point in the container runtime. The binary is run by a high-level container runtime, @@ -108,7 +65,8 @@ fn main() -> Result<()> { let opts = Opts::parse(); - if let Err(e) = crate::logger::init(opts.debug, opts.log, opts.log_format) { + if let Err(e) = crate::logger::init(opts.global.debug, opts.global.log, opts.global.log_format) + { eprintln!("log init failed: {:?}", e); } @@ -117,24 +75,31 @@ fn main() -> Result<()> { nix::unistd::geteuid(), std::env::args_os() ); - let root_path = determine_root_path(opts.root)?; - let systemd_cgroup = opts.systemd_cgroup; + let root_path = determine_root_path(opts.global.root)?; + let systemd_cgroup = opts.global.systemd_cgroup; match opts.subcmd { - SubCommand::Create(create) => create.exec(root_path, systemd_cgroup), - SubCommand::Start(start) => start.exec(root_path), - SubCommand::Run(run) => run.exec(root_path, systemd_cgroup), - SubCommand::Exec(exec) => exec.exec(root_path), - SubCommand::Kill(kill) => kill.exec(root_path), - SubCommand::Delete(delete) => delete.exec(root_path), - SubCommand::State(state) => state.exec(root_path), - SubCommand::Info(info) => info.exec(), - SubCommand::List(list) => list.exec(root_path), - SubCommand::Spec(spec) => spec.exec(), - SubCommand::Pause(pause) => pause.exec(root_path), - SubCommand::Resume(resume) => resume.exec(root_path), - SubCommand::Events(events) => events.exec(root_path), - SubCommand::Ps(ps) => ps.exec(root_path), + SubCommand::Standard(cmd) => match cmd { + StandardCmd::Create(create) => { + commands::create::create(create, root_path, systemd_cgroup) + } + StandardCmd::Start(start) => commands::start::start(start, root_path), + StandardCmd::Kill(kill) => commands::kill::kill(kill, root_path), + StandardCmd::Delete(delete) => commands::delete::delete(delete, root_path), + StandardCmd::State(state) => commands::state::state(state, root_path), + }, + SubCommand::Common(cmd) => match cmd { + CommonCmd::Events(events) => commands::events::events(events, root_path), + CommonCmd::Exec(exec) => commands::exec::exec(exec, root_path), + CommonCmd::List(list) => commands::list::list(list, root_path), + CommonCmd::Pause(pause) => commands::pause::pause(pause, root_path), + CommonCmd::Ps(ps) => commands::ps::ps(ps, root_path), + CommonCmd::Resume(resume) => commands::resume::resume(resume, root_path), + CommonCmd::Run(run) => commands::run::run(run, root_path, systemd_cgroup), + CommonCmd::Spec(spec) => commands::spec_json::spec(spec), + }, + + SubCommand::Info(info) => commands::info::info(info), } }