diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20b95d5..977ef5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,12 @@ jobs: - run: cargo check --examples --tests --all-targets - run: cargo fmt --all -- --check --files-with-diff - run: cargo clippy --all-targets --all-features -- -D warnings - - run: cargo test --all-features + # runc::tests::test_exec needs $XDG_RUNTIME_DIR to be set + - env: + XDG_RUNTIME_DIR: /tmp/dummy-xdr + run: | + mkdir -p /tmp/dummy-xdr + cargo test --all-features deny: name: Deny diff --git a/Cargo.toml b/Cargo.toml index 787436b..6741814 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,6 @@ members = [ "crates/logging", "crates/shim-protos", "crates/shim", - "crates/snapshots" + "crates/snapshots", + "crates/runc" ] diff --git a/crates/client/examples/container.rs b/crates/client/examples/container.rs index fc74e5b..8265ac1 100644 --- a/crates/client/examples/container.rs +++ b/crates/client/examples/container.rs @@ -158,7 +158,7 @@ async fn main() { // test container output let actual_stdout = fs::read_to_string(stdout).expect("read stdout actual"); - assert_eq!(actual_stdout.strip_suffix("\n").unwrap(), output); + assert_eq!(actual_stdout.strip_suffix('\n').unwrap(), output); // clear stdin/stdout/stderr let _ = fs::remove_dir_all(tmp); diff --git a/crates/runc/Cargo.toml b/crates/runc/Cargo.toml new file mode 100644 index 0000000..6bd72b0 --- /dev/null +++ b/crates/runc/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "runc" +version = "0.1.0" +authors = ["Yuna Tomida ", "The containerd Authors"] +edition = "2018" +license = "Apache-2.0" +repository = "https://github.com/containerd/rust-extensions" +keywords = ["containerd", "containers", "runc"] +description = "A crate for consuming the runc binary in your Rust applications" +homepage = "https://containerd.io" + + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[dependencies] +async-trait = "0.1.52" +futures = "0.3.19" +libc = "0.2.112" +log = "0.4.14" +nix = "0.23.1" +oci-spec = "0.5.4" +path-absolutize = "3.0.11" +rand = "0.8.4" +serde = { version = "1.0.133", features = ["derive"] } +serde_json = "1.0.74" +tempfile = "3.3.0" +thiserror = "1.0.30" +time = { version = "0.3.7", features = ["serde", "std"] } +tokio = { version = "1.15.0", features = ["full"] } +uuid = { version = "0.8.2", features = ["v4"] } diff --git a/crates/runc/README.md b/crates/runc/README.md new file mode 100644 index 0000000..77a43a2 --- /dev/null +++ b/crates/runc/README.md @@ -0,0 +1,43 @@ +# Rust binding for runc client + +A crate for consuming the runc binary in your Rust applications, similar to [go-runc](https://github.com/containerd/go-runc) for Go. +This crate is based on archived [rust-runc](https://github.com/pwFoo/rust-runc). + +## Usage +Both sync/async version is available. +You can build runc client with `RuncConfig` in method chaining style. +Call `build()` or `build_async()` to get client. +Note that async client depends on [tokio](https://github.com/tokio-rs/tokio), then please use it on tokio runtime. + +```rust +use runc; + +#[tokio::main] +async fn main() { + let config = runc::Config::new() + .root("./new_root") + .debug(false) + .log("/path/to/logfile.json") + .log_format(runc::LogFormat::Json) + .rootless(true); + + let client = config.build_async().unwrap(); + + let opts = runc::options::CreateOpts::new() + .pid_file("/path/to/pid/file") + .no_pivot(true); + + client.create("container-id", "path/to/bundle", Some(&opts)).unwrap(); +} +``` + +## Limitations +- Supported commands are only: + - create + - start + - state + - kill + - delete +- Exec is **not** available in `RuncAsyncClient` now. +- Console utilites are **not** available + - see [Go version](https://github.com/containerd/go-runc/blob/main/console.go) \ No newline at end of file diff --git a/crates/runc/src/container.rs b/crates/runc/src/container.rs new file mode 100644 index 0000000..1351ca2 --- /dev/null +++ b/crates/runc/src/container.rs @@ -0,0 +1,87 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Forked from https://github.com/pwFoo/rust-runc/blob/313e6ae5a79b54455b0a242a795c69adf035141a/src/lib.rs + +/* + * Copyright 2020 fsyncd, Berlin, Germany. + * Additional material, copyright of the containerd authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use time::serde::timestamp; +use time::OffsetDateTime; + +/// Information for runc container +#[derive(Debug, Serialize, Deserialize)] +pub struct Container { + pub id: String, + pub pid: usize, + pub status: String, + pub bundle: String, + pub rootfs: String, + #[serde(with = "timestamp")] + pub created: OffsetDateTime, + pub annotations: HashMap, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde_test() { + let j = r#" + { + "id": "fake", + "pid": 1000, + "status": "RUNNING", + "bundle": "/path/to/bundle", + "rootfs": "/path/to/rootfs", + "created": 1431684000, + "annotations": { + "foo": "bar" + } + }"#; + + let c: Container = serde_json::from_str(j).unwrap(); + assert_eq!(c.id, "fake"); + assert_eq!(c.pid, 1000); + assert_eq!(c.status, "RUNNING"); + assert_eq!(c.bundle, "/path/to/bundle"); + assert_eq!(c.rootfs, "/path/to/rootfs"); + assert_eq!( + c.created, + OffsetDateTime::from_unix_timestamp(1431684000).unwrap() + ); + assert_eq!(c.annotations.get("foo"), Some(&"bar".to_string())); + assert_eq!(c.annotations.get("bar"), None); + } +} diff --git a/crates/runc/src/error.rs b/crates/runc/src/error.rs new file mode 100644 index 0000000..65b1aa6 --- /dev/null +++ b/crates/runc/src/error.rs @@ -0,0 +1,119 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Forked from https://github.com/pwFoo/rust-runc/blob/313e6ae5a79b54455b0a242a795c69adf035141a/src/lib.rs + +/* + * Copyright 2020 fsyncd, Berlin, Germany. + * Additional material, copyright of the containerd authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::env; +use std::io; +use std::process::ExitStatus; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Unable to extract test files: {0}")] + BundleExtractFailed(io::Error), + + #[error("Invalid path: {0}")] + InvalidPath(io::Error), + + #[error(transparent)] + JsonDeserializationFailed(#[from] serde_json::error::Error), + + #[error("Missing container statistics")] + MissingContainerStats, + + #[error(transparent)] + ProcessSpawnFailed(io::Error), + + #[error("Error occured in runc: {0}")] + InvalidCommand(io::Error), + + #[error("Runc command failed: status={status}, stdout=\"{stdout}\", stderr=\"{stderr}\"")] + CommandFailed { + status: ExitStatus, + stdout: String, + stderr: String, + }, + + #[error("Runc IO unavailable: {0}")] + UnavailableIO(io::Error), + + #[error("Runc command timed out: {0}")] + CommandTimeout(tokio::time::error::Elapsed), + + #[error("Unable to parse runc version")] + InvalidVersion, + + #[error("Unable to locate the runc")] + NotFound, + + #[error("Error occurs with fs: {0}")] + FileSystemError(io::Error), + + #[error("Failed to spec file: {0}")] + SpecFileCreationFailed(io::Error), + + #[error(transparent)] + SpecFileCleanupFailed(io::Error), + + #[error("Failed to find valid path for spec file")] + SpecFileNotFound, + + #[error("Top command is missing a pid header")] + TopMissingPidHeader, + + #[error("Top command returned an empty response")] + TopShortResponseError, + + #[error("Unix socket connection error: {0}")] + UnixSocketConnectionFailed(io::Error), + + #[error("Unable to bind to unix socket: {0}")] + UnixSocketBindFailed(io::Error), + + #[error("Unix socket failed to receive pty")] + UnixSocketReceiveMessageFailed, + + #[error("Unix socket unexpectedly closed")] + UnixSocketClosed, + + #[error("Failed to handle environment variable: {0}")] + EnvError(env::VarError), + + #[error("Sorry, this part of api is not implemented: {0}")] + Unimplemented(String), + + #[error("Error occured in runc client: {0}")] + Other(Box), +} diff --git a/crates/runc/src/events.rs b/crates/runc/src/events.rs new file mode 100644 index 0000000..9bf7125 --- /dev/null +++ b/crates/runc/src/events.rs @@ -0,0 +1,181 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Forked from https://github.com/pwFoo/rust-runc/blob/313e6ae5a79b54455b0a242a795c69adf035141a/src/events.rs + +/* + * Copyright 2020 fsyncd, Berlin, Germany. + * Additional material, copyright of the containerd authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +/// Event type generated by runc +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all(serialize = "lowercase", deserialize = "lowercase"))] +pub enum EventType { + /// Statistics + Stats, + /// Out of memory + Oom, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Event { + #[serde(rename = "type")] + pub event_type: EventType, + pub id: String, + #[serde(rename = "data")] + pub stats: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Stats { + pub cpu: Cpu, + pub memory: Memory, + pub pids: Pids, + #[serde(rename = "blkio")] + pub block_io: BlkIO, + #[serde(rename = "hugetlb")] + pub huge_tlb: HugeTLB, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HugeTLB { + pub usage: Option, + pub max: Option, + #[serde(rename = "failcnt")] + pub fail_count: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BlkIOEntry { + pub major: Option, + pub minor: Option, + pub op: Option, + pub value: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BlkIO { + /// Number of bytes transferred to and from the disk + #[serde(rename = "ioServiceBytesRecursive")] + pub io_service_bytes_recursive: Option>, + /// Number of io requests issued to the disk + #[serde(rename = "ioServicedRecursive")] + pub io_serviced_recursive: Option>, + /// Number of queued disk io requests + #[serde(rename = "ioQueueRecursive")] + pub io_queued_recursive: Option>, + /// Amount of time io requests took to service + #[serde(rename = "ioServiceTimeRecursive")] + pub io_service_time_recursive: Option>, + /// Amount of time io requests spent waiting in the queue + #[serde(rename = "ioWaitTimeRecursive")] + pub io_wait_time_recursive: Option>, + /// Number of merged io requests + #[serde(rename = "ioMergedRecursive")] + pub io_merged_recursive: Option>, + /// Disk time allocated the device + #[serde(rename = "ioTimeRecursive")] + pub io_time_recursive: Option>, + /// Number of sectors transferred to and from the io device + #[serde(rename = "sectorsRecursive")] + pub sectors_recursive: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Pids { + /// Number of pids in the cgroup + pub current: Option, + /// Active pids hard limit + pub limit: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Throttling { + /// Number of periods with throttling active + pub periods: Option, + #[serde(rename = "throttledPeriods")] + /// Number of periods when the container hit its throttling limit + pub throtted_periods: Option, + /// Aggregate time the container was throttled for in nanoseconds + #[serde(rename = "throttledTime")] + pub throtted_time: Option, +} + +/// Each members represents time in nanoseconds +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CpuUsage { + /// Total CPU time consumed + pub total: Option, + /// Total CPU time consumed per core + pub per_cpu: Option>, + /// Total CPU time consumed in kernel mode + pub kernel: u64, + /// Total CPU time consumed in user mode + pub user: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Cpu { + pub usage: Option, + pub throttling: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MemoryEntry { + /// Memory limit in bytes + pub limit: u64, + /// Usage in bytes + pub usage: Option, + /// Maximum usage in bytes + pub max: Option, + /// Count of memory allocation failures + #[serde(rename = "failcnt")] + pub fail_count: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Memory { + /// Memory usage for cache + pub cache: Option, + /// Overall memory usage, excluding swap + pub usage: Option, + /// Overall memory usage, including swap + pub swap: Option, + /// Kernel usage of memory + pub kernel: Option, + /// Kernel TCP of memory + #[serde(rename = "kernelTCP")] + pub kernel_tcp: Option, + /// Raw stats of memory + pub raw: Option>, +} diff --git a/crates/runc/src/io.rs b/crates/runc/src/io.rs new file mode 100644 index 0000000..3d4364b --- /dev/null +++ b/crates/runc/src/io.rs @@ -0,0 +1,305 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +use std::fmt::{self, Debug, Formatter}; +use std::fs::File; +use std::os::unix::io::FromRawFd; +use std::os::unix::prelude::AsRawFd; +use std::process::Command; +use std::sync::Mutex; + +use nix::fcntl::OFlag; +use nix::sys::stat::Mode; +use nix::unistd::{Gid, Uid}; + +pub trait Io: Sync + Send { + /// Return write side of stdin + fn stdin(&self) -> Option { + None + } + + /// Return read side of stdout + fn stdout(&self) -> Option { + None + } + + /// Return read side of stderr + fn stderr(&self) -> Option { + None + } + + /// Set IO for passed command. + /// Read side of stdin, write side of stdout and write side of stderr should be provided to command. + fn set(&self, _cmd: &mut Command) -> std::io::Result<()>; + + // tokio version of set() + fn set_tk(&self, _cmd: &mut tokio::process::Command) -> std::io::Result<()>; + + fn close_after_start(&self); +} + +impl Debug for dyn Io { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "Io",) + } +} + +#[derive(Debug, Clone)] +pub struct IOOption { + pub open_stdin: bool, + pub open_stdout: bool, + pub open_stderr: bool, +} + +impl Default for IOOption { + fn default() -> Self { + Self { + open_stdin: true, + open_stdout: true, + open_stderr: true, + } + } +} + +/// This struct represents pipe that can be used to transfer +/// stdio inputs and outputs +/// when one side of closed, this struct represent it with [`None`] +#[derive(Debug)] +pub struct Pipe { + // Might be ugly hack: using mutex in order to take rd/wr under immutable [`Pipe`] + rd: Mutex>, + wr: Mutex>, +} + +impl Pipe { + pub fn new() -> std::io::Result { + let (r, w) = nix::unistd::pipe()?; + let (rd, wr) = unsafe { + ( + Mutex::new(Some(File::from_raw_fd(r))), + Mutex::new(Some(File::from_raw_fd(w))), + ) + }; + Ok(Self { rd, wr }) + } + + pub fn take_read(&self) -> Option { + let mut m = self.rd.lock().unwrap(); + m.take() + } + + pub fn take_write(&self) -> Option { + let mut m = self.wr.lock().unwrap(); + m.take() + } + + pub fn close_read(&self) { + let mut m = self.rd.lock().unwrap(); + let _ = m.take(); + } + + pub fn close_write(&self) { + let mut m = self.wr.lock().unwrap(); + let _ = m.take(); + } +} + +#[derive(Debug)] +pub struct PipedIo { + stdin: Option, + stdout: Option, + stderr: Option, +} + +impl PipedIo { + pub fn new(uid: u32, gid: u32, opts: IOOption) -> std::io::Result { + let uid = Some(Uid::from_raw(uid)); + let gid = Some(Gid::from_raw(gid)); + let stdin = if opts.open_stdin { + let pipe = Pipe::new()?; + { + let m = pipe.rd.lock().unwrap(); + if let Some(f) = m.as_ref() { + nix::unistd::fchown(f.as_raw_fd(), uid, gid)?; + } + } + Some(pipe) + } else { + None + }; + + let stdout = if opts.open_stdout { + let pipe = Pipe::new()?; + { + let m = pipe.wr.lock().unwrap(); + if let Some(f) = m.as_ref() { + nix::unistd::fchown(f.as_raw_fd(), uid, gid)?; + } + } + Some(pipe) + } else { + None + }; + + let stderr = if opts.open_stderr { + let pipe = Pipe::new()?; + { + let m = pipe.wr.lock().unwrap(); + if let Some(f) = m.as_ref() { + nix::unistd::fchown(f.as_raw_fd(), uid, gid)?; + } + } + Some(pipe) + } else { + None + }; + + Ok(Self { + stdin, + stdout, + stderr, + }) + } +} + +impl Io for PipedIo { + fn stdin(&self) -> Option { + if let Some(ref stdin) = self.stdin { + stdin.take_write() + } else { + None + } + } + + fn stdout(&self) -> Option { + if let Some(ref stdout) = self.stdout { + stdout.take_read() + } else { + None + } + } + + fn stderr(&self) -> Option { + if let Some(ref stderr) = self.stderr { + stderr.take_read() + } else { + None + } + } + + /// Note that this internally use [`std::fs::File`]'s [`try_clone()`]. + /// Thus, the files passed to commands will be not closed after command exit. + fn set(&self, cmd: &mut Command) -> std::io::Result<()> { + if let Some(ref p) = self.stdin { + let m = p.rd.lock().unwrap(); + if let Some(stdin) = &*m { + let f = stdin.try_clone()?; + cmd.stdin(f); + } + } + + if let Some(ref p) = self.stdout { + let m = p.wr.lock().unwrap(); + if let Some(f) = &*m { + let f = f.try_clone()?; + cmd.stdout(f); + } + } + + if let Some(ref p) = self.stderr { + let m = p.wr.lock().unwrap(); + if let Some(f) = &*m { + let f = f.try_clone()?; + cmd.stderr(f); + } + } + Ok(()) + } + + fn set_tk(&self, cmd: &mut tokio::process::Command) -> std::io::Result<()> { + if let Some(ref p) = self.stdin { + let m = p.rd.lock().unwrap(); + if let Some(stdin) = &*m { + let f = stdin.try_clone()?; + cmd.stdin(f); + } + } + + if let Some(ref p) = self.stdout { + let m = p.wr.lock().unwrap(); + if let Some(f) = &*m { + let f = f.try_clone()?; + cmd.stdout(f); + } + } + + if let Some(ref p) = self.stderr { + let m = p.wr.lock().unwrap(); + if let Some(f) = &*m { + let f = f.try_clone()?; + cmd.stderr(f); + } + } + Ok(()) + } + + /// closing only write side (should be stdout/err "from" runc process) + fn close_after_start(&self) { + if let Some(ref p) = self.stdout { + p.close_write(); + } + if let Some(ref p) = self.stderr { + p.close_write(); + } + } +} + +// IO setup for /dev/null use with runc +#[derive(Debug)] +pub struct NullIo { + dev_null: Mutex>, +} + +impl NullIo { + pub fn new() -> std::io::Result { + let fd = nix::fcntl::open("/dev/null", OFlag::O_RDONLY, Mode::empty())?; + let dev_null = unsafe { Mutex::new(Some(std::fs::File::from_raw_fd(fd))) }; + Ok(Self { dev_null }) + } +} + +impl Io for NullIo { + fn set(&self, cmd: &mut Command) -> std::io::Result<()> { + if let Some(null) = self.dev_null.lock().unwrap().as_ref() { + cmd.stdout(null.try_clone()?); + cmd.stderr(null.try_clone()?); + } + Ok(()) + } + + fn set_tk(&self, cmd: &mut tokio::process::Command) -> std::io::Result<()> { + if let Some(null) = self.dev_null.lock().unwrap().as_ref() { + cmd.stdout(null.try_clone()?); + cmd.stderr(null.try_clone()?); + } + Ok(()) + } + + /// closing only write side (should be stdout/err "from" runc process) + fn close_after_start(&self) { + let mut m = self.dev_null.lock().unwrap(); + let _ = m.take(); + } +} diff --git a/crates/runc/src/lib.rs b/crates/runc/src/lib.rs new file mode 100644 index 0000000..c61aa42 --- /dev/null +++ b/crates/runc/src/lib.rs @@ -0,0 +1,980 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Forked from https://github.com/pwFoo/rust-runc/blob/313e6ae5a79b54455b0a242a795c69adf035141a/src/lib.rs + +/* + * Copyright 2020 fsyncd, Berlin, Germany. + * Additional material, copyright of the containerd authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! A crate for consuming the runc binary in your Rust applications, similar to [go-runc](https://github.com/containerd/go-runc) for Go. + +use std::fmt::{self, Display}; +use std::path::Path; +use std::process::{ExitStatus, Output, Stdio}; +use std::time::Duration; + +use oci_spec::runtime::{Linux, Process}; + +// suspended for difficulties +// pub mod console; +pub mod container; +pub mod error; +pub mod events; +pub mod io; +pub mod monitor; +pub mod options; +mod runc; +mod utils; + +use crate::container::Container; +use crate::error::Error; +use crate::events::{Event, Stats}; +use crate::monitor::{DefaultMonitor, Exit, ProcessMonitor}; +use crate::options::*; +use crate::utils::{JSON, TEXT}; + +type Result = std::result::Result; + +/// Response is for (pid, exit status, outputs). +#[derive(Debug, Clone)] +pub struct Response { + pub pid: u32, + pub status: ExitStatus, + pub output: String, +} + +#[derive(Debug, Clone)] +pub struct Version { + pub runc_version: Option, + pub spec_version: Option, + pub commit: Option, +} + +#[derive(Debug, Clone)] +pub enum LogFormat { + Json, + Text, +} + +impl Display for LogFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LogFormat::Json => write!(f, "{}", JSON), + LogFormat::Text => write!(f, "{}", TEXT), + } + } +} + +/// Configuration for runc client. +/// +/// This struct provide chaining interface like, for example, [`std::fs::OpenOptions`]. +/// Note that you cannot access the members of Config directly. +/// +/// # Example +/// +/// ```ignore +/// use runc::{LogFormat, Config}; +/// +/// let config = Config::new() +/// .root("./new_root") +/// .debug(false) +/// .log("/path/to/logfile.json") +/// .log_format(LogFormat::Json) +/// .rootless(true); +/// let client = config.build(); +/// ``` +#[derive(Debug, Clone, Default)] +pub struct Config(runc::Config); + +impl Config { + pub fn new() -> Self { + Self::default() + } + + pub fn command

(&mut self, command: P) -> &mut Self + where + P: AsRef, + { + self.0.command(command); + self + } + + pub fn root

(&mut self, root: P) -> &mut Self + where + P: AsRef, + { + self.0.root(root); + self + } + + pub fn debug(&mut self, debug: bool) -> &mut Self { + self.0.debug(debug); + self + } + + pub fn log

(&mut self, log: P) -> &mut Self + where + P: AsRef, + { + self.0.log(log); + self + } + + pub fn log_format(&mut self, log_format: LogFormat) -> &mut Self { + self.0.log_format(log_format); + self + } + + pub fn log_format_json(&mut self) -> &mut Self { + self.0.log_format_json(); + self + } + + pub fn log_format_text(&mut self) -> &mut Self { + self.0.log_format_text(); + self + } + + pub fn systemd_cgroup(&mut self, systemd_cgroup: bool) -> &mut Self { + self.0.systemd_cgroup(systemd_cgroup); + self + } + + // FIXME: criu is not supported now + // pub fn criu(mut self, criu: bool) -> Self { + // self.0.criu(criu); + // self + // } + + pub fn rootless(&mut self, rootless: bool) -> &mut Self { + self.0.rootless(rootless); + self + } + + pub fn set_pgid(&mut self, set_pgid: bool) -> &mut Self { + self.0.set_pgid(set_pgid); + self + } + + pub fn rootless_auto(&mut self) -> &mut Self { + self.0.rootless_auto(); + self + } + + pub fn timeout(&mut self, millis: u64) -> &mut Self { + self.0.timeout(millis); + self + } + + pub fn build(&mut self) -> Result { + Ok(Client(self.0.build()?)) + } + + pub fn build_async(&mut self) -> Result { + Ok(AsyncClient(self.0.build()?)) + } +} + +#[derive(Debug, Clone)] +pub struct Client(runc::Runc); + +impl Client { + /// Create a new runc client from the supplied configuration + pub fn from_config(mut config: Config) -> Result { + config.build() + } + + #[cfg(target_os = "linux")] + pub fn command(&self, args: &[String]) -> Result { + let args = [&self.0.args()?, args].concat(); + let mut cmd = std::process::Command::new(&self.0.command); + cmd.args(&args).env_remove("NOTIFY_SOCKET"); // NOTIFY_SOCKET introduces a special behavior in runc but should only be set if invoked from systemd + Ok(cmd) + } + + #[cfg(not(target_os = "linux"))] + pub fn command(&self, _args: &[String]) -> Result { + Err(Error::Unimplemented("command".to_string())) + } + + pub fn checkpoint(&self) -> Result<()> { + Err(Error::Unimplemented("checkpoint".to_string())) + } + + fn launch(&self, mut cmd: std::process::Command, combined_output: bool) -> Result { + let child = cmd.spawn().map_err(Error::ProcessSpawnFailed)?; + let pid = child.id(); + let result = child.wait_with_output().map_err(Error::InvalidCommand)?; + let status = result.status; + let stdout = String::from_utf8(result.stdout).unwrap(); + let stderr = String::from_utf8(result.stderr).unwrap(); + if status.success() { + if combined_output { + Ok(Response { + pid, + status, + output: stdout + stderr.as_str(), + }) + } else { + Ok(Response { + pid, + status, + output: stdout, + }) + } + } else { + Err(Error::CommandFailed { + status, + stdout, + stderr, + }) + } + } + + /// Create a new container + pub fn create

(&self, id: &str, bundle: P, opts: Option<&CreateOpts>) -> Result + where + P: AsRef, + { + let mut args = vec![ + "create".to_string(), + "--bundle".to_string(), + utils::abs_string(bundle)?, + ]; + if let Some(opts) = opts { + args.append(&mut opts.args()?); + } + args.push(id.to_string()); + let mut cmd = self.command(&args)?; + match opts { + Some(CreateOpts { io: Some(_io), .. }) => { + _io.set(&mut cmd).map_err(Error::UnavailableIO)?; + let res = self.launch(cmd, true)?; + _io.close_after_start(); + Ok(res) + } + _ => self.launch(cmd, true), + } + } + + /// Delete a container + pub fn delete(&self, id: &str, opts: Option<&DeleteOpts>) -> Result<()> { + let mut args = vec!["delete".to_string()]; + if let Some(opts) = opts { + args.append(&mut opts.args()); + } + args.push(id.to_string()); + self.launch(self.command(&args)?, true)?; + Ok(()) + } + + /// Execute an additional process inside the container + pub fn exec(&self, id: &str, spec: &Process, opts: Option<&ExecOpts>) -> Result<()> { + let filename = utils::temp_filename_in_runtime_dir()?; + let spec_json = serde_json::to_string(spec).map_err(Error::JsonDeserializationFailed)?; + std::fs::write(&filename, spec_json).map_err(Error::SpecFileCreationFailed)?; + let mut args = vec!["exec".to_string(), "process".to_string(), filename]; + if let Some(opts) = opts { + args.append(&mut opts.args()?); + } + args.push(id.to_string()); + let mut cmd = self.command(&args)?; + if let Some(ExecOpts { io: Some(_io), .. }) = opts { + _io.set(&mut cmd).map_err(Error::UnavailableIO)?; + } + let _ = self.launch(cmd, true)?; + Ok(()) + } + + /// Send the specified signal to processes inside the container + pub fn kill(&self, id: &str, sig: u32, opts: Option<&KillOpts>) -> Result<()> { + let mut args = vec!["kill".to_string()]; + if let Some(opts) = opts { + args.append(&mut opts.args()); + } + args.push(id.to_string()); + args.push(sig.to_string()); + let _ = self.launch(self.command(&args)?, true)?; + Ok(()) + } + + /// List all containers associated with this runc instance + pub fn list(&self) -> Result> { + let args = ["list".to_string(), "--format-json".to_string()]; + let res = self.launch(self.command(&args)?, true)?; + let output = res.output.trim(); + // Ugly hack to work around golang + Ok(if output == "null" { + Vec::new() + } else { + serde_json::from_str(output).map_err(Error::JsonDeserializationFailed)? + }) + } + + /// Pause a container + pub fn pause(&self, id: &str) -> Result<()> { + let args = ["pause".to_string(), id.to_string()]; + let _ = self.launch(self.command(&args)?, true)?; + Ok(()) + } + + pub fn restore(&self) -> Result<()> { + Err(Error::Unimplemented("restore".to_string())) + } + + /// Resume a container + pub fn resume(&self, id: &str) -> Result<()> { + let args = ["pause".to_string(), id.to_string()]; + let _ = self.launch(self.command(&args)?, true)?; + Ok(()) + } + + /// Run the create, start, delete lifecycle of the container and return its exit status + pub fn run

(&self, id: &str, bundle: P, opts: Option<&CreateOpts>) -> Result + where + P: AsRef, + { + let mut args = vec!["run".to_string(), "--bundle".to_string()]; + if let Some(opts) = opts { + args.append(&mut opts.args()?); + } + args.push(utils::abs_string(bundle)?); + args.push(id.to_string()); + let mut cmd = self.command(&args)?; + if let Some(CreateOpts { io: Some(_io), .. }) = opts { + _io.set(&mut cmd).map_err(Error::UnavailableIO)?; + }; + self.launch(self.command(&args)?, true) + } + + /// Start an already created container + pub fn start(&self, id: &str) -> Result { + let args = ["start".to_string(), id.to_string()]; + self.launch(self.command(&args)?, true) + } + + /// Return the state of a container + pub fn state(&self, id: &str) -> Result { + let args = ["state".to_string(), id.to_string()]; + let res = self.launch(self.command(&args)?, true)?; + serde_json::from_str(&res.output).map_err(Error::JsonDeserializationFailed) + } + + /// Update a container with the provided resource spec + pub fn update(&self, id: &str, resources: &Linux) -> Result<()> { + let filename = utils::temp_filename_in_runtime_dir()?; + let spec_json = + serde_json::to_string(resources).map_err(Error::JsonDeserializationFailed)?; + std::fs::write(&filename, spec_json).map_err(Error::SpecFileCreationFailed)?; + let args = [ + "update".to_string(), + "--resources".to_string(), + filename, + id.to_string(), + ]; + self.launch(self.command(&args)?, true)?; + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub struct AsyncClient(runc::Runc); + +// As monitor instance never have to be mutable (it has only &self methods), declare it as const. +const MONITOR: DefaultMonitor = DefaultMonitor::new(); + +/// Async client for runc +/// Note that you MUST use this client on tokio runtime, as this client internally use [`tokio::process::Command`] +/// and some other utilities. +impl AsyncClient { + /// Create a new runc client from the supplied configuration + pub fn from_config(mut config: Config) -> Result { + config.build_async() + } + + pub fn command(&self, args: &[String]) -> Result { + let args = [&self.0.args()?, args].concat(); + let mut cmd = tokio::process::Command::new(&self.0.command); + cmd.stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()); + cmd.args(&args).env_remove("NOTIFY_SOCKET"); // NOTIFY_SOCKET introduces a special behavior in runc but should only be set if invoked from systemd + Ok(cmd) + } + + pub async fn launch( + &self, + cmd: tokio::process::Command, + combined_output: bool, + ) -> Result { + let (tx, rx) = tokio::sync::oneshot::channel::(); + let start = MONITOR.start(cmd, tx); + let wait = MONITOR.wait(rx); + let ( + Output { + status, + stdout, + stderr, + }, + Exit { pid, .. }, + ) = tokio::try_join!(start, wait).map_err(Error::InvalidCommand)?; + + // ugly hack to work around + let stdout = String::from_utf8(stdout) + .expect("returned non-utf8 characters from container process."); + let stderr = String::from_utf8(stderr) + .expect("returned non-utf8 characters from container process."); + + if status.success() { + if combined_output { + Ok(Response { + pid, + status, + output: stdout + stderr.as_str(), + }) + } else { + Ok(Response { + pid, + status, + output: stdout, + }) + } + } else { + Err(Error::CommandFailed { + status, + stdout, + stderr, + }) + } + } + + pub async fn checkpoint(&self) -> Result<()> { + Err(Error::Unimplemented("checkpoint".to_string())) + } + + /// Create a new container + pub async fn create

(&self, id: &str, bundle: P, opts: Option<&CreateOpts>) -> Result<()> + where + P: AsRef, + { + let mut args = vec![ + "create".to_string(), + "--bundle".to_string(), + utils::abs_string(bundle)?, + ]; + if let Some(opts) = opts { + args.append(&mut opts.args()?); + } + args.push(id.to_string()); + let mut cmd = self.command(&args)?; + match opts { + Some(CreateOpts { io: Some(_io), .. }) => { + _io.set_tk(&mut cmd).map_err(Error::UnavailableIO)?; + let (tx, rx) = tokio::sync::oneshot::channel::(); + let start = MONITOR.start(cmd, tx); + let wait = MONITOR.wait(rx); + let ( + Output { + status, + stdout, + stderr, + }, + _, + ) = tokio::try_join!(start, wait).map_err(Error::InvalidCommand)?; + _io.close_after_start(); + + let stdout = String::from_utf8(stdout).unwrap(); + let stderr = String::from_utf8(stderr).unwrap(); + if !status.success() { + return Err(Error::CommandFailed { + status, + stdout, + stderr, + }); + } + } + _ => { + let _ = self.launch(cmd, true).await?; + } + } + Ok(()) + } + + /// Delete a container + pub async fn delete(&self, id: &str, opts: Option<&DeleteOpts>) -> Result<()> { + let mut args = vec!["delete".to_string()]; + if let Some(opts) = opts { + args.append(&mut opts.args()); + } + args.push(id.to_string()); + let _ = self.launch(self.command(&args)?, true).await?; + Ok(()) + } + + /// Return an event stream of container notifications + pub async fn events(&self, _id: &str, _interval: &Duration) -> Result<()> { + Err(Error::Unimplemented("events".to_string())) + } + + /// Execute an additional process inside the container + pub async fn exec(&self, _id: &str, _spec: &Process, _opts: Option<&ExecOpts>) -> Result<()> { + Err(Error::Unimplemented("exec".to_string())) + } + + /// Send the specified signal to processes inside the container + pub async fn kill(&self, id: &str, sig: u32, opts: Option<&KillOpts>) -> Result<()> { + let mut args = vec!["kill".to_string()]; + if let Some(opts) = opts { + args.append(&mut opts.args()); + } + args.push(id.to_string()); + args.push(sig.to_string()); + let _ = self.launch(self.command(&args)?, true).await?; + Ok(()) + } + + /// List all containers associated with this runc instance + pub async fn list(&self) -> Result> { + let args = ["list".to_string(), "--format-json".to_string()]; + let res = self.launch(self.command(&args)?, true).await?; + let output = res.output.trim(); + // Ugly hack to work around golang + Ok(if output == "null" { + Vec::new() + } else { + serde_json::from_str(output).map_err(Error::JsonDeserializationFailed)? + }) + } + + /// Pause a container + pub async fn pause(&self, id: &str) -> Result<()> { + let args = ["pause".to_string(), id.to_string()]; + let _ = self.launch(self.command(&args)?, true).await?; + Ok(()) + } + + /// List all the processes inside the container, returning their pids + pub async fn ps(&self, id: &str) -> Result> { + let args = [ + "ps".to_string(), + "--format-json".to_string(), + id.to_string(), + ]; + let res = self.launch(self.command(&args)?, true).await?; + let output = res.output.trim(); + // Ugly hack to work around golang + Ok(if output == "null" { + Vec::new() + } else { + serde_json::from_str(output).map_err(Error::JsonDeserializationFailed)? + }) + } + + pub async fn restore(&self) -> Result<()> { + Err(Error::Unimplemented("restore".to_string())) + } + + /// Resume a container + pub async fn resume(&self, id: &str) -> Result<()> { + let args = ["pause".to_string(), id.to_string()]; + let _ = self.launch(self.command(&args)?, true).await?; + Ok(()) + } + + /// Run the create, start, delete lifecycle of the container and return its exit status + pub async fn run

(&self, id: &str, bundle: P, opts: Option<&CreateOpts>) -> Result<()> + where + P: AsRef, + { + let mut args = vec!["run".to_string(), "--bundle".to_string()]; + if let Some(opts) = opts { + args.append(&mut opts.args()?); + } + args.push(utils::abs_string(bundle)?); + args.push(id.to_string()); + let _ = self.launch(self.command(&args)?, true).await?; + Ok(()) + } + + /// Start an already created container + pub async fn start(&self, id: &str) -> Result<()> { + let args = vec!["start".to_string(), id.to_string()]; + let _ = self.launch(self.command(&args)?, true).await?; + Ok(()) + } + + /// Return the state of a container + pub async fn state(&self, id: &str) -> Result> { + let args = vec!["state".to_string(), id.to_string()]; + let res = self.launch(self.command(&args)?, true).await?; + serde_json::from_str(&res.output).map_err(Error::JsonDeserializationFailed) + } + + /// Return the latest statistics for a container + pub async fn stats(&self, id: &str) -> Result { + let args = vec!["events".to_string(), "--stats".to_string(), id.to_string()]; + let res = self.launch(self.command(&args)?, true).await?; + let event: Event = + serde_json::from_str(&res.output).map_err(Error::JsonDeserializationFailed)?; + if let Some(stats) = event.stats { + Ok(stats) + } else { + Err(Error::MissingContainerStats) + } + } + + /// Update a container with the provided resource spec + pub async fn update(&self, id: &str, resources: &Linux) -> Result<()> { + let filename = utils::temp_filename_in_runtime_dir()?; + let spec_json = + serde_json::to_string(resources).map_err(Error::JsonDeserializationFailed)?; + std::fs::write(&filename, spec_json).map_err(Error::SpecFileCreationFailed)?; + let args = vec![ + "update".to_string(), + "--resources".to_string(), + filename, + id.to_string(), + ]; + let _ = self.launch(self.command(&args)?, true).await?; + Ok(()) + } +} + +#[cfg(test)] +#[cfg(target_os = "linux")] +mod tests { + use super::*; + + // following style of go-runc, use only true/false to test + const CMD_TRUE: &str = "/bin/true"; + const CMD_FALSE: &str = "/bin/false"; + + fn ok_client() -> Client { + Config::new() + .command(CMD_TRUE) + .build() + .expect("unable to create runc instance") + } + + fn fail_client() -> Client { + Config::new() + .command(CMD_FALSE) + .build() + .expect("unable to create runc instance") + } + + fn ok_async_client() -> AsyncClient { + Config::new() + .command(CMD_TRUE) + .build_async() + .expect("unable to create runc instance") + } + + fn fail_async_client() -> AsyncClient { + Config::new() + .command(CMD_FALSE) + .build_async() + .expect("unable to create runc instance") + } + + fn dummy_process() -> Process { + serde_json::from_str( + " + { + \"user\": { + \"uid\": 1000, + \"gid\": 1000 + }, + \"cwd\": \"/path/to/dir\" + }", + ) + .unwrap() + } + + #[test] + fn test_create() { + let opts = CreateOpts::new(); + let ok_runc = ok_client(); + ok_runc + .create("fake-id", "fake-bundle", Some(&opts)) + .expect("true failed."); + eprintln!("ok_runc succeeded."); + let fail_runc = fail_client(); + match fail_runc.create("fake-id", "fake-bundle", Some(&opts)) { + Ok(_) => panic!("fail_runc returned exit status 0."), + Err(Error::CommandFailed { + status, + stdout, + stderr, + }) => { + if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() { + eprintln!("fail_runc succeeded."); + } else { + panic!("unexpected outputs from fail_runc.") + } + } + Err(e) => panic!("unexpected error from fail_runc: {:?}", e), + } + } + + #[test] + fn test_run() { + let opts = CreateOpts::new(); + let ok_runc = ok_client(); + ok_runc + .run("fake-id", "fake-bundle", Some(&opts)) + .expect("true failed."); + eprintln!("ok_runc succeeded."); + let fail_runc = fail_client(); + match fail_runc.run("fake-id", "fake-bundle", Some(&opts)) { + Ok(_) => panic!("fail_runc returned exit status 0."), + Err(Error::CommandFailed { + status, + stdout, + stderr, + }) => { + if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() { + eprintln!("fail_runc succeeded."); + } else { + panic!("unexpected outputs from fail_runc.") + } + } + Err(e) => panic!("unexpected error from fail_runc: {:?}", e), + } + } + + #[test] + fn test_exec() { + let opts = ExecOpts::new(); + let ok_runc = ok_client(); + let proc = dummy_process(); + ok_runc + .exec("fake-id", &proc, Some(&opts)) + .expect("true failed."); + eprintln!("ok_runc succeeded."); + let fail_runc = fail_client(); + match fail_runc.exec("fake-id", &proc, Some(&opts)) { + Ok(_) => panic!("fail_runc returned exit status 0."), + Err(Error::CommandFailed { + status, + stdout, + stderr, + }) => { + if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() { + eprintln!("fail_runc succeeded."); + } else { + panic!("unexpected outputs from fail_runc.") + } + } + Err(e) => panic!("unexpected error from fail_runc: {:?}", e), + } + } + + #[test] + fn test_delete() { + let opts = DeleteOpts::new(); + let ok_runc = ok_client(); + ok_runc + .delete("fake-id", Some(&opts)) + .expect("true failed."); + eprintln!("ok_runc succeeded."); + let fail_runc = fail_client(); + match fail_runc.delete("fake-id", Some(&opts)) { + Ok(_) => panic!("fail_runc returned exit status 0."), + Err(Error::CommandFailed { + status, + stdout, + stderr, + }) => { + if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() { + eprintln!("fail_runc succeeded."); + } else { + panic!("unexpected outputs from fail_runc.") + } + } + Err(e) => panic!("unexpected error from fail_runc: {:?}", e), + } + } + + #[tokio::test] + async fn test_async_create() { + let opts = CreateOpts::new(); + let ok_runc = ok_async_client(); + let ok_task = tokio::spawn(async move { + ok_runc + .create("fake-id", "fake-bundle", Some(&opts)) + .await + .expect("true failed."); + eprintln!("ok_runc succeeded."); + }); + + let opts = CreateOpts::new(); + let fail_runc = fail_async_client(); + let fail_task = tokio::spawn(async move { + match fail_runc + .create("fake-id", "fake-bundle", Some(&opts)) + .await + { + Ok(_) => panic!("fail_runc returned exit status 0."), + Err(Error::CommandFailed { + status, + stdout, + stderr, + }) => { + if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() { + eprintln!("fail_runc succeeded."); + } else { + panic!("unexpected outputs from fail_runc.") + } + } + Err(e) => panic!("unexpected error from fail_runc: {:?}", e), + } + }); + + ok_task.await.expect("ok_task failed."); + fail_task.await.expect("fail_task unexpectedly succeeded."); + } + + #[tokio::test] + async fn test_async_start() { + let ok_runc = ok_async_client(); + let ok_task = tokio::spawn(async move { + ok_runc.start("fake-id").await.expect("true failed."); + eprintln!("ok_runc succeeded."); + }); + + let fail_runc = fail_async_client(); + let fail_task = tokio::spawn(async move { + match fail_runc.start("fake-id").await { + Ok(_) => panic!("fail_runc returned exit status 0."), + Err(Error::CommandFailed { + status, + stdout, + stderr, + }) => { + if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() { + eprintln!("fail_runc succeeded."); + } else { + panic!("unexpected outputs from fail_runc.") + } + } + Err(e) => panic!("unexpected error from fail_runc: {:?}", e), + } + }); + + ok_task.await.expect("ok_task failed."); + fail_task.await.expect("fail_task unexpectedly succeeded."); + } + + #[tokio::test] + async fn test_async_run() { + let opts = CreateOpts::new(); + let ok_runc = Config::new() + .command(CMD_TRUE) + .build_async() + .expect("unable to create runc instance"); + tokio::spawn(async move { + ok_runc + .create("fake-id", "fake-bundle", Some(&opts)) + .await + .expect("true failed."); + eprintln!("ok_runc succeeded."); + }); + + let opts = CreateOpts::new(); + let fail_runc = Config::new() + .command(CMD_FALSE) + .build_async() + .expect("unable to create runc instance"); + tokio::spawn(async move { + match fail_runc + .create("fake-id", "fake-bundle", Some(&opts)) + .await + { + Ok(_) => panic!("fail_runc returned exit status 0."), + Err(Error::CommandFailed { + status, + stdout, + stderr, + }) => { + if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() { + eprintln!("fail_runc succeeded."); + } else { + panic!("unexpected outputs from fail_runc.") + } + } + Err(e) => panic!("unexpected error from fail_runc: {:?}", e), + } + }) + .await + .expect("tokio spawn falied."); + } + + #[tokio::test] + async fn test_async_delete() { + let opts = DeleteOpts::new(); + let ok_runc = Config::new() + .command(CMD_TRUE) + .build_async() + .expect("unable to create runc instance"); + tokio::spawn(async move { + ok_runc + .delete("fake-id", Some(&opts)) + .await + .expect("true failed."); + eprintln!("ok_runc succeeded."); + }); + + let opts = DeleteOpts::new(); + let fail_runc = Config::new() + .command(CMD_FALSE) + .build_async() + .expect("unable to create runc instance"); + tokio::spawn(async move { + match fail_runc.delete("fake-id", Some(&opts)).await { + Ok(_) => panic!("fail_runc returned exit status 0."), + Err(Error::CommandFailed { + status, + stdout, + stderr, + }) => { + if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() { + eprintln!("fail_runc succeeded."); + } else { + panic!("unexpected outputs from fail_runc.") + } + } + Err(e) => panic!("unexpected error from fail_runc: {:?}", e), + } + }) + .await + .expect("tokio spawn falied."); + } +} diff --git a/crates/runc/src/monitor.rs b/crates/runc/src/monitor.rs new file mode 100644 index 0000000..4379e94 --- /dev/null +++ b/crates/runc/src/monitor.rs @@ -0,0 +1,80 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +use std::process::Output; + +use async_trait::async_trait; +use log::error; +use time::OffsetDateTime; +use tokio::sync::oneshot::{Receiver, Sender}; + +// ProcessMonitor for handling runc process exit +// Implementation is different from Go's, because if you return Sender in start() and want to +// use it in wait(), then start and wait cannot be executed concurrently. +// Alternatively, caller of start() and wait() have to prepare channel +#[async_trait] +pub trait ProcessMonitor { + /// Caller cand choose [`std::mem::forget`] about resource + /// associated to that command, e.g. file descriptors. + async fn start( + &self, + mut cmd: tokio::process::Command, + tx: Sender, + ) -> std::io::Result { + let chi = cmd.spawn()?; + let pid = chi + .id() + .expect("failed to take pid of the container process."); + let out = chi.wait_with_output().await?; + let ts = OffsetDateTime::now_utc(); + match tx.send(Exit { + ts, + pid, + status: out.status.code().unwrap(), + }) { + Ok(_) => Ok(out), + Err(e) => { + error!("command {:?} exited but receiver dropped.", cmd); + error!("couldn't send messages: {:?}", e); + Err(std::io::ErrorKind::ConnectionRefused.into()) + } + } + } + async fn wait(&self, rx: Receiver) -> std::io::Result { + rx.await.map_err(|_| { + error!("sender dropped."); + std::io::ErrorKind::BrokenPipe.into() + }) + } +} + +#[derive(Debug, Clone, Default)] +pub struct DefaultMonitor {} + +impl ProcessMonitor for DefaultMonitor {} + +impl DefaultMonitor { + pub const fn new() -> Self { + Self {} + } +} + +#[derive(Debug)] +pub struct Exit { + pub ts: OffsetDateTime, + pub pid: u32, + pub status: i32, +} diff --git a/crates/runc/src/options.rs b/crates/runc/src/options.rs new file mode 100644 index 0000000..4f3d129 --- /dev/null +++ b/crates/runc/src/options.rs @@ -0,0 +1,371 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Forked from https://github.com/pwFoo/rust-runc/blob/313e6ae5a79b54455b0a242a795c69adf035141a/src/lib.rs + +/* + * Copyright 2020 fsyncd, Berlin, Germany. + * Additional material, copyright of the containerd authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use crate::error::Error; +use crate::io::Io; +use crate::utils::{self, ALL, CONSOLE_SOCKET, DETACH, FORCE, NO_NEW_KEYRING, NO_PIVOT, PID_FILE}; + +pub trait Args { + type Output; + fn args(&self) -> Self::Output; +} + +#[derive(Debug, Clone, Default)] +pub struct CreateOpts { + pub io: Option>, + /// Path to where a pid file should be created. + pub pid_file: Option, + /// Path to where a console socket should be created. + pub console_socket: Option, + /// Detach from the container's process (only available for run) + pub detach: bool, + /// Don't use pivot_root to jail process inside rootfs. + pub no_pivot: bool, + /// A new session keyring for the container will not be created. + pub no_new_keyring: bool, +} + +impl Args for CreateOpts { + type Output = Result, Error>; + fn args(&self) -> Self::Output { + let mut args: Vec = vec![]; + if let Some(pid_file) = &self.pid_file { + args.push(PID_FILE.to_string()); + args.push(utils::abs_string(pid_file)?); + } + if let Some(console_socket) = &self.console_socket { + args.push(CONSOLE_SOCKET.to_string()); + args.push(utils::abs_string(console_socket)?); + } + if self.no_pivot { + args.push(NO_PIVOT.to_string()); + } + if self.no_new_keyring { + args.push(NO_NEW_KEYRING.to_string()); + } + if self.detach { + args.push(DETACH.to_string()); + } + Ok(args) + } +} + +impl CreateOpts { + pub fn new() -> Self { + Self::default() + } + + pub fn io(mut self, io: Arc) -> Self { + self.io = Some(io); + self + } + + pub fn pid_file

(mut self, pid_file: P) -> Self + where + P: AsRef, + { + self.pid_file = Some(pid_file.as_ref().to_path_buf()); + self + } + + pub fn console_socket

(mut self, console_socket: P) -> Self + where + P: AsRef, + { + self.console_socket = Some(console_socket.as_ref().to_path_buf()); + self + } + + pub fn detach(mut self, detach: bool) -> Self { + self.detach = detach; + self + } + + pub fn no_pivot(mut self, no_pivot: bool) -> Self { + self.no_pivot = no_pivot; + self + } + + pub fn no_new_keyring(mut self, no_new_keyring: bool) -> Self { + self.no_new_keyring = no_new_keyring; + self + } +} + +/// Container execution options +#[derive(Debug, Clone, Default)] +pub struct ExecOpts { + pub io: Option>, + /// Path to where a pid file should be created. + pub pid_file: Option, + /// Path to where a console socket should be created. + pub console_socket: Option, + /// Detach from the container's process (only available for run) + pub detach: bool, +} + +impl Args for ExecOpts { + type Output = Result, Error>; + fn args(&self) -> Self::Output { + let mut args: Vec = vec![]; + if let Some(pid_file) = &self.pid_file { + args.push(PID_FILE.to_string()); + args.push(utils::abs_string(pid_file)?); + } + if let Some(console_socket) = &self.console_socket { + args.push(CONSOLE_SOCKET.to_string()); + args.push(utils::abs_string(console_socket)?); + } + if self.detach { + args.push(DETACH.to_string()); + } + Ok(args) + } +} + +impl ExecOpts { + pub fn new() -> Self { + Self::default() + } + + pub fn io(mut self, io: Arc) -> Self { + self.io = Some(io); + self + } + + pub fn pid_file

(mut self, pid_file: P) -> Self + where + P: AsRef, + { + self.pid_file = Some(pid_file.as_ref().to_path_buf()); + self + } + + pub fn console_socket

(mut self, console_socket: P) -> Self + where + P: AsRef, + { + self.console_socket = Some(console_socket.as_ref().to_path_buf()); + self + } + + pub fn detach(mut self, detach: bool) -> Self { + self.detach = detach; + self + } +} + +/// Container deletion options +#[derive(Debug, Clone, Default)] +pub struct DeleteOpts { + /// Forcibly delete the container if it is still running + pub force: bool, +} + +impl Args for DeleteOpts { + type Output = Vec; + fn args(&self) -> Self::Output { + let mut args: Vec = vec![]; + if self.force { + args.push(FORCE.to_string()); + } + args + } +} + +impl DeleteOpts { + pub fn new() -> Self { + Self::default() + } + + pub fn force(mut self, force: bool) -> Self { + self.force = force; + self + } +} + +/// Container killing options +#[derive(Debug, Clone, Default)] +pub struct KillOpts { + /// Seng the kill signal to all the processes inside the container + pub all: bool, +} + +impl Args for KillOpts { + type Output = Vec; + fn args(&self) -> Self::Output { + let mut args: Vec = vec![]; + if self.all { + args.push(ALL.to_string()); + } + args + } +} + +impl KillOpts { + pub fn new() -> Self { + Self::default() + } + + pub fn all(mut self, all: bool) -> Self { + self.all = all; + self + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use std::env; + + const ARGS_FAIL_MSG: &str = "Args.args() failed."; + + #[test] + fn create_opts_test() { + assert_eq!( + CreateOpts::new().args().expect(ARGS_FAIL_MSG), + vec![String::new(); 0] + ); + + assert_eq!( + CreateOpts::new().pid_file(".").args().expect(ARGS_FAIL_MSG), + vec![ + "--pid-file".to_string(), + env::current_dir() + .unwrap() + .to_string_lossy() + .parse::() + .unwrap() + ] + ); + + assert_eq!( + CreateOpts::new() + .console_socket("..") + .args() + .expect(ARGS_FAIL_MSG), + vec![ + "--console-socket".to_string(), + env::current_dir() + .unwrap() + .parent() + .unwrap() + .to_string_lossy() + .parse::() + .unwrap() + ] + ); + + assert_eq!( + CreateOpts::new() + .detach(true) + .no_pivot(true) + .no_new_keyring(true) + .args() + .expect(ARGS_FAIL_MSG), + vec![ + "--no-pivot".to_string(), + "--no-new-keyring".to_string(), + "--detach".to_string(), + ] + ); + } + + #[test] + fn exec_opts_test() { + assert_eq!( + ExecOpts::new().args().expect(ARGS_FAIL_MSG), + vec![String::new(); 0] + ); + + assert_eq!( + ExecOpts::new().pid_file(".").args().expect(ARGS_FAIL_MSG), + vec![ + "--pid-file".to_string(), + env::current_dir() + .unwrap() + .to_string_lossy() + .parse::() + .unwrap() + ] + ); + + assert_eq!( + ExecOpts::new() + .console_socket("..") + .args() + .expect(ARGS_FAIL_MSG), + vec![ + "--console-socket".to_string(), + env::current_dir() + .unwrap() + .parent() + .unwrap() + .to_string_lossy() + .parse::() + .unwrap() + ] + ); + + assert_eq!( + ExecOpts::new().detach(true).args().expect(ARGS_FAIL_MSG), + vec!["--detach".to_string(),] + ); + } + + #[test] + fn delete_opts_test() { + assert_eq!( + DeleteOpts::new().force(false).args(), + vec![String::new(); 0] + ); + + assert_eq!( + DeleteOpts::new().force(true).args(), + vec!["--force".to_string()], + ); + } + + #[test] + fn kill_opts_test() { + assert_eq!(KillOpts::new().all(false).args(), vec![String::new(); 0]); + + assert_eq!(KillOpts::new().all(true).args(), vec!["--all".to_string()],); + } +} diff --git a/crates/runc/src/runc.rs b/crates/runc/src/runc.rs new file mode 100644 index 0000000..9f8038e --- /dev/null +++ b/crates/runc/src/runc.rs @@ -0,0 +1,210 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Forked from https://github.com/pwFoo/rust-runc/blob/313e6ae5a79b54455b0a242a795c69adf035141a/src/lib.rs + +/* + * Copyright 2020 fsyncd, Berlin, Germany. + * Additional material, copyright of the containerd authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::path::{Path, PathBuf}; +use std::time::Duration; + +use crate::error::Error; +use crate::options::Args; +use crate::utils::{self, DEBUG, DEFAULT_COMMAND, LOG, LOG_FORMAT, ROOT, ROOTLESS, SYSTEMD_CGROUP}; +use crate::LogFormat; + +/// Inner struct for runc configuration +#[derive(Debug, Clone, Default)] +pub struct Config { + /// This field is set to overrides the name of the runc binary. If [`None`], "runc" is used. + command: Option, + /// Path to root directory of container rootfs. + root: Option, + /// Debug logging. If true, debug level logs are emitted. + debug: bool, + /// Path to log file. + log: Option, + /// Specifyng log format. Here, json or text is available. Default is "text" and interpreted as text if [`None`]. + log_format: Option, + // FIXME: implementation of pdeath_signal is suspended due to difficulties, maybe it's favorable to use signal-hook crate. + // pdeath_signal: XXX, + /// Using systemd cgroup. + systemd_cgroup: bool, + /// Setting process group ID(gpid). + set_pgid: bool, + // FIXME: implementation of extra_args is suspended due to difficulties. + // criu: String, + /// Setting of whether using rootless mode or not. If [`None`], "auto" settings is used. Note that "auto" is different from explicit "true" or "false". + rootless: Option, + // FIXME: implementation of extra_args is suspended due to difficulties. + // extra_args: Vec, + /// Timeout settings for runc command. Default is 5 seconds. + /// This will be used only in AsyncClient. + timeout: Option, +} + +impl Config { + pub fn command

(&mut self, command: P) + where + P: AsRef, + { + self.command = Some(command.as_ref().to_path_buf()); + } + + pub fn root

(&mut self, root: P) + where + P: AsRef, + { + self.root = Some(root.as_ref().to_path_buf()); + } + + pub fn debug(&mut self, debug: bool) { + self.debug = debug; + } + + pub fn log

(&mut self, log: P) + where + P: AsRef, + { + self.log = Some(log.as_ref().to_path_buf()); + } + + pub fn log_format(&mut self, log_format: LogFormat) { + self.log_format = Some(log_format); + } + + pub fn log_format_json(&mut self) { + self.log_format = Some(LogFormat::Json); + } + + pub fn log_format_text(&mut self) { + self.log_format = Some(LogFormat::Text); + } + + pub fn systemd_cgroup(&mut self, systemd_cgroup: bool) { + self.systemd_cgroup = systemd_cgroup; + } + + // FIXME: criu is not supported now + // pub fn criu(&mut self, criu: bool) { + // self.criu = criu; + // } + + pub fn rootless(&mut self, rootless: bool) { + self.rootless = Some(rootless); + } + + pub fn set_pgid(&mut self, set_pgid: bool) { + self.set_pgid = set_pgid; + } + + pub fn rootless_auto(&mut self) { + let _ = self.rootless.take(); + } + + pub fn timeout(&mut self, millis: u64) { + self.timeout = Some(Duration::from_millis(millis)); + } + + pub fn build(&mut self) -> Result { + let command = utils::binary_path( + self.command + .clone() + .unwrap_or_else(|| PathBuf::from(DEFAULT_COMMAND)), + ) + .ok_or(Error::NotFound)?; + Ok(Runc { + command, + root: self.root.clone(), + debug: self.debug, + log: self.log.clone(), + log_format: self.log_format.clone().unwrap_or(LogFormat::Text), + // self.pdeath_signal: self.pdeath_signal, + systemd_cgroup: self.systemd_cgroup, + set_pgid: self.set_pgid, + // criu: self.criu, + rootless: self.rootless, + // extra_args: self.extra_args, + timeout: self.timeout.unwrap_or_else(|| Duration::from_millis(5000)), + }) + } +} + +/// Inner Runtime for RuncClient/RuncAsyncClient +#[derive(Debug, Clone)] +pub struct Runc { + pub command: PathBuf, + pub root: Option, + pub debug: bool, + pub log: Option, + pub log_format: LogFormat, + // pdeath_signal: XXX, + pub set_pgid: bool, + // criu: bool, + pub systemd_cgroup: bool, + pub rootless: Option, + // extra_args: Vec, + pub timeout: Duration, +} + +impl Args for Runc { + type Output = Result, Error>; + fn args(&self) -> Self::Output { + let mut args: Vec = vec![]; + if let Some(root) = &self.root { + args.push(ROOT.to_string()); + args.push(utils::abs_string(root)?); + } + if self.debug { + args.push(DEBUG.to_string()); + } + if let Some(log_path) = &self.log { + args.push(LOG.to_string()); + args.push(utils::abs_string(log_path)?); + } + args.push(LOG_FORMAT.to_string()); + args.push(self.log_format.to_string()); + // if self.criu { + // args.push(CRIU.to_string()); + // } + if self.systemd_cgroup { + args.push(SYSTEMD_CGROUP.to_string()); + } + if let Some(rootless) = self.rootless { + let arg = format!("{}={}", ROOTLESS, rootless); + args.push(arg); + } + // if self.extra_args.len() > 0 { + // args.append(&mut self.extra_args.clone()) + // } + Ok(args) + } +} diff --git a/crates/runc/src/utils.rs b/crates/runc/src/utils.rs new file mode 100644 index 0000000..7cee541 --- /dev/null +++ b/crates/runc/src/utils.rs @@ -0,0 +1,116 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#![allow(unused)] + +use std::env; +use std::path::{Path, PathBuf}; + +use path_absolutize::*; +use tempfile::{Builder, NamedTempFile}; +use uuid::Uuid; + +use crate::error::Error; + +// constants for flags +pub const ALL: &str = "--all"; +pub const CONSOLE_SOCKET: &str = "--console-socket"; +// pub const CRIU: &str = "--criu"; +pub const DEBUG: &str = "--debug"; +pub const DETACH: &str = "--detach"; +pub const FORCE: &str = "--force"; +pub const LOG: &str = "--log"; +pub const LOG_FORMAT: &str = "--log-format"; +pub const NO_NEW_KEYRING: &str = "--no-new-keyring"; +pub const NO_PIVOT: &str = "--no-pivot"; +pub const PID_FILE: &str = "--pid-file"; +pub const ROOT: &str = "--root"; +pub const ROOTLESS: &str = "--rootless"; +pub const SYSTEMD_CGROUP: &str = "--systemd-cgroup"; + +// constants for log format +pub const JSON: &str = "json"; +pub const TEXT: &str = "text"; + +// constant for command +pub const DEFAULT_COMMAND: &str = "runc"; + +// helper to resolve path (such as path for runc binary, pid files, etc. ) +pub fn abs_path_buf

(path: P) -> Result +where + P: AsRef, +{ + Ok(path + .as_ref() + .absolutize() + .map_err(Error::InvalidPath)? + .to_path_buf()) +} + +pub fn abs_string

(path: P) -> Result +where + P: AsRef, +{ + Ok(abs_path_buf(path)? + .to_string_lossy() + .parse::() + .unwrap()) +} + +pub fn temp_filename_in_runtime_dir() -> Result { + env::var_os("XDG_RUNTIME_DIR") + .map(|runtime_dir| { + format!( + "{}/runc-process-{}", + runtime_dir.to_string_lossy().parse::().unwrap(), + Uuid::new_v4(), + ) + }) + .ok_or(Error::SpecFileNotFound) +} + +pub fn make_temp_file_in_runtime_dir() -> Result<(NamedTempFile, String), Error> { + let file_name = env::var_os("XDG_RUNTIME_DIR") + .map(|runtime_dir| { + format!( + "{}/runc-process-{}", + runtime_dir.to_string_lossy().parse::().unwrap(), + Uuid::new_v4(), + ) + }) + .ok_or(Error::SpecFileNotFound)?; + let temp_file = Builder::new() + .prefix(&file_name) + .tempfile() + .map_err(Error::SpecFileCreationFailed)?; + Ok((temp_file, file_name)) +} + +pub fn binary_path

(path: P) -> Option +where + P: AsRef, +{ + env::var_os("PATH").and_then(|paths| { + env::split_paths(&paths).find_map(|dir| { + let full_path = dir.join(path.as_ref()); + if full_path.is_file() { + Some(full_path) + } else { + None + } + }) + }) +} diff --git a/crates/shim-protos/src/cgroups/metrics.rs b/crates/shim-protos/src/cgroups/metrics.rs index 6ac30ed..870ef50 100644 --- a/crates/shim-protos/src/cgroups/metrics.rs +++ b/crates/shim-protos/src/cgroups/metrics.rs @@ -1,4 +1,4 @@ -// This file is generated by rust-protobuf 2.25.2. Do not edit +// This file is generated by rust-protobuf 2.27.1. Do not edit // @generated // https://github.com/rust-lang/rust-clippy/issues/702 @@ -21,7 +21,7 @@ /// Generated files are compatible only with the same version /// of protobuf runtime. -// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_25_2; +// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_27_1; #[derive(PartialEq,Clone,Default)] pub struct Metrics { diff --git a/crates/shim-protos/src/events/container.rs b/crates/shim-protos/src/events/container.rs index 4443645..49206c0 100644 --- a/crates/shim-protos/src/events/container.rs +++ b/crates/shim-protos/src/events/container.rs @@ -1,4 +1,4 @@ -// This file is generated by rust-protobuf 2.25.2. Do not edit +// This file is generated by rust-protobuf 2.27.1. Do not edit // @generated // https://github.com/rust-lang/rust-clippy/issues/702 @@ -21,7 +21,7 @@ /// Generated files are compatible only with the same version /// of protobuf runtime. -// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_25_2; +// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_27_1; #[derive(PartialEq,Clone,Default)] pub struct ContainerCreate { diff --git a/crates/shim-protos/src/events/content.rs b/crates/shim-protos/src/events/content.rs index 8097468..c75f8b5 100644 --- a/crates/shim-protos/src/events/content.rs +++ b/crates/shim-protos/src/events/content.rs @@ -1,4 +1,4 @@ -// This file is generated by rust-protobuf 2.25.2. Do not edit +// This file is generated by rust-protobuf 2.27.1. Do not edit // @generated // https://github.com/rust-lang/rust-clippy/issues/702 @@ -21,7 +21,7 @@ /// Generated files are compatible only with the same version /// of protobuf runtime. -// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_25_2; +// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_27_1; #[derive(PartialEq,Clone,Default)] pub struct ContentDelete { diff --git a/crates/shim-protos/src/events/image.rs b/crates/shim-protos/src/events/image.rs index 249ff54..b5f5a91 100644 --- a/crates/shim-protos/src/events/image.rs +++ b/crates/shim-protos/src/events/image.rs @@ -1,4 +1,4 @@ -// This file is generated by rust-protobuf 2.25.2. Do not edit +// This file is generated by rust-protobuf 2.27.1. Do not edit // @generated // https://github.com/rust-lang/rust-clippy/issues/702 @@ -21,7 +21,7 @@ /// Generated files are compatible only with the same version /// of protobuf runtime. -// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_25_2; +// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_27_1; #[derive(PartialEq,Clone,Default)] pub struct ImageCreate { diff --git a/crates/shim-protos/src/events/mount.rs b/crates/shim-protos/src/events/mount.rs index 77669a5..b07151e 100644 --- a/crates/shim-protos/src/events/mount.rs +++ b/crates/shim-protos/src/events/mount.rs @@ -1,4 +1,4 @@ -// This file is generated by rust-protobuf 2.25.2. Do not edit +// This file is generated by rust-protobuf 2.27.1. Do not edit // @generated // https://github.com/rust-lang/rust-clippy/issues/702 @@ -21,7 +21,7 @@ /// Generated files are compatible only with the same version /// of protobuf runtime. -// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_25_2; +// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_27_1; #[derive(PartialEq,Clone,Default)] pub struct Mount { diff --git a/crates/shim-protos/src/events/namespace.rs b/crates/shim-protos/src/events/namespace.rs index 5a84b60..58b9dbe 100644 --- a/crates/shim-protos/src/events/namespace.rs +++ b/crates/shim-protos/src/events/namespace.rs @@ -1,4 +1,4 @@ -// This file is generated by rust-protobuf 2.25.2. Do not edit +// This file is generated by rust-protobuf 2.27.1. Do not edit // @generated // https://github.com/rust-lang/rust-clippy/issues/702 @@ -21,7 +21,7 @@ /// Generated files are compatible only with the same version /// of protobuf runtime. -// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_25_2; +// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_27_1; #[derive(PartialEq,Clone,Default)] pub struct NamespaceCreate { diff --git a/crates/shim-protos/src/events/snapshot.rs b/crates/shim-protos/src/events/snapshot.rs index f4d2e22..4de22f9 100644 --- a/crates/shim-protos/src/events/snapshot.rs +++ b/crates/shim-protos/src/events/snapshot.rs @@ -1,4 +1,4 @@ -// This file is generated by rust-protobuf 2.25.2. Do not edit +// This file is generated by rust-protobuf 2.27.1. Do not edit // @generated // https://github.com/rust-lang/rust-clippy/issues/702 @@ -21,7 +21,7 @@ /// Generated files are compatible only with the same version /// of protobuf runtime. -// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_25_2; +// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_27_1; #[derive(PartialEq,Clone,Default)] pub struct SnapshotPrepare { diff --git a/crates/shim-protos/src/events/task.rs b/crates/shim-protos/src/events/task.rs index 7841b8f..16f1cd8 100644 --- a/crates/shim-protos/src/events/task.rs +++ b/crates/shim-protos/src/events/task.rs @@ -1,4 +1,4 @@ -// This file is generated by rust-protobuf 2.25.2. Do not edit +// This file is generated by rust-protobuf 2.27.1. Do not edit // @generated // https://github.com/rust-lang/rust-clippy/issues/702 @@ -21,7 +21,7 @@ /// Generated files are compatible only with the same version /// of protobuf runtime. -// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_25_2; +// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_27_1; #[derive(PartialEq,Clone,Default)] pub struct TaskCreate { diff --git a/crates/shim-protos/src/shim/empty.rs b/crates/shim-protos/src/shim/empty.rs index fabba56..9827104 100644 --- a/crates/shim-protos/src/shim/empty.rs +++ b/crates/shim-protos/src/shim/empty.rs @@ -1,4 +1,4 @@ -// This file is generated by rust-protobuf 2.25.2. Do not edit +// This file is generated by rust-protobuf 2.27.1. Do not edit // @generated // https://github.com/rust-lang/rust-clippy/issues/702 @@ -21,7 +21,7 @@ /// Generated files are compatible only with the same version /// of protobuf runtime. -// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_25_2; +// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_27_1; #[derive(PartialEq,Clone,Default)] pub struct Empty { diff --git a/crates/shim-protos/src/shim/events.rs b/crates/shim-protos/src/shim/events.rs index 2a3f04b..622beb2 100644 --- a/crates/shim-protos/src/shim/events.rs +++ b/crates/shim-protos/src/shim/events.rs @@ -1,4 +1,4 @@ -// This file is generated by rust-protobuf 2.25.2. Do not edit +// This file is generated by rust-protobuf 2.27.1. Do not edit // @generated // https://github.com/rust-lang/rust-clippy/issues/702 @@ -21,7 +21,7 @@ /// Generated files are compatible only with the same version /// of protobuf runtime. -// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_25_2; +// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_27_1; #[derive(PartialEq,Clone,Default)] pub struct ForwardRequest { diff --git a/crates/shim-protos/src/shim/mount.rs b/crates/shim-protos/src/shim/mount.rs index 77669a5..b07151e 100644 --- a/crates/shim-protos/src/shim/mount.rs +++ b/crates/shim-protos/src/shim/mount.rs @@ -1,4 +1,4 @@ -// This file is generated by rust-protobuf 2.25.2. Do not edit +// This file is generated by rust-protobuf 2.27.1. Do not edit // @generated // https://github.com/rust-lang/rust-clippy/issues/702 @@ -21,7 +21,7 @@ /// Generated files are compatible only with the same version /// of protobuf runtime. -// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_25_2; +// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_27_1; #[derive(PartialEq,Clone,Default)] pub struct Mount { diff --git a/crates/shim-protos/src/shim/shim.rs b/crates/shim-protos/src/shim/shim.rs index 5830a7c..d58a371 100644 --- a/crates/shim-protos/src/shim/shim.rs +++ b/crates/shim-protos/src/shim/shim.rs @@ -1,4 +1,4 @@ -// This file is generated by rust-protobuf 2.25.2. Do not edit +// This file is generated by rust-protobuf 2.27.1. Do not edit // @generated // https://github.com/rust-lang/rust-clippy/issues/702 @@ -21,7 +21,7 @@ /// Generated files are compatible only with the same version /// of protobuf runtime. -// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_25_2; +// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_27_1; #[derive(PartialEq,Clone,Default)] pub struct CreateTaskRequest { diff --git a/crates/shim-protos/src/shim/task.rs b/crates/shim-protos/src/shim/task.rs index e205a79..ac05d53 100644 --- a/crates/shim-protos/src/shim/task.rs +++ b/crates/shim-protos/src/shim/task.rs @@ -1,4 +1,4 @@ -// This file is generated by rust-protobuf 2.25.2. Do not edit +// This file is generated by rust-protobuf 2.27.1. Do not edit // @generated // https://github.com/rust-lang/rust-clippy/issues/702 @@ -21,7 +21,7 @@ /// Generated files are compatible only with the same version /// of protobuf runtime. -// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_25_2; +// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_27_1; #[derive(PartialEq,Clone,Default)] pub struct Process { diff --git a/crates/shim/src/args.rs b/crates/shim/src/args.rs index 4333edf..b77f159 100644 --- a/crates/shim/src/args.rs +++ b/crates/shim/src/args.rs @@ -51,7 +51,7 @@ pub enum Error { MissingArg(String), /// Syntax error. #[error("Parse failed: {0}")] - ParseError(String), + ParseFailed(String), } /// Parses command line arguments passed to the shim. @@ -72,7 +72,7 @@ pub fn parse>(args: &[S]) -> Result { FlagError::BadFlag { flag } => Error::InvalidArg(flag), FlagError::UnknownFlag { name } => Error::InvalidArg(name), FlagError::ArgumentNeeded { name } => Error::MissingArg(name), - FlagError::ParseError { error } => Error::ParseError(format!("{:?}", error)), + FlagError::ParseError { error } => Error::ParseFailed(format!("{:?}", error)), })?; if let Some(action) = args.get(0) { diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 3441d80..9d3bdc5 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.52" +channel = "1.54" components = ["rustfmt", "clippy"]