From d34fa454adfb556d079770c8b28b803ec9d02970 Mon Sep 17 00:00:00 2001 From: Yuna Tomida Date: Thu, 3 Feb 2022 02:19:22 +0000 Subject: [PATCH 01/10] runc client Signed-off-by: Yuna Tomida --- Cargo.toml | 3 +- crates/runc-rust/Cargo.toml | 29 + crates/runc-rust/README.md | 43 ++ crates/runc-rust/src/container.rs | 53 ++ crates/runc-rust/src/error.rs | 119 ++++ crates/runc-rust/src/events.rs | 181 ++++++ crates/runc-rust/src/io.rs | 333 ++++++++++ crates/runc-rust/src/lib.rs | 1009 +++++++++++++++++++++++++++++ crates/runc-rust/src/monitor.rs | 86 +++ crates/runc-rust/src/options.rs | 371 +++++++++++ crates/runc-rust/src/runc.rs | 210 ++++++ crates/runc-rust/src/utils.rs | 102 +++ rust-toolchain.toml | 2 +- 13 files changed, 2539 insertions(+), 2 deletions(-) create mode 100644 crates/runc-rust/Cargo.toml create mode 100644 crates/runc-rust/README.md create mode 100644 crates/runc-rust/src/container.rs create mode 100644 crates/runc-rust/src/error.rs create mode 100644 crates/runc-rust/src/events.rs create mode 100644 crates/runc-rust/src/io.rs create mode 100644 crates/runc-rust/src/lib.rs create mode 100644 crates/runc-rust/src/monitor.rs create mode 100644 crates/runc-rust/src/options.rs create mode 100644 crates/runc-rust/src/runc.rs create mode 100644 crates/runc-rust/src/utils.rs diff --git a/Cargo.toml b/Cargo.toml index 787436b..ec5bed1 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-rust" ] diff --git a/crates/runc-rust/Cargo.toml b/crates/runc-rust/Cargo.toml new file mode 100644 index 0000000..e624c2f --- /dev/null +++ b/crates/runc-rust/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" +chrono = { version = "0.4.19", features = ["serde"] } +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" +tokio = { version = "1.15.0", features = ["full"] } +uuid = { version = "0.8.2", features = ["v4"] } diff --git a/crates/runc-rust/README.md b/crates/runc-rust/README.md new file mode 100644 index 0000000..b372f3c --- /dev/null +++ b/crates/runc-rust/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_rust as runc; + +#[tokio::main] +async fn main() { + let config = runc::RuncConfig::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 = 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-rust/src/container.rs b/crates/runc-rust/src/container.rs new file mode 100644 index 0000000..aab8b6b --- /dev/null +++ b/crates/runc-rust/src/container.rs @@ -0,0 +1,53 @@ +/* + 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 chrono::serde::ts_seconds_option; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// 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 = "ts_seconds_option")] + pub created: Option>, + pub annotations: HashMap, +} diff --git a/crates/runc-rust/src/error.rs b/crates/runc-rust/src/error.rs new file mode 100644 index 0000000..62d23ce --- /dev/null +++ b/crates/runc-rust/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}")] + SpecFileCreationError(io::Error), + + #[error(transparent)] + SpecFileCleanupError(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")] + UnixSocketReceiveMessageError, + + #[error("Unix socket unexpectedly closed")] + UnixSocketUnexpectedCloseError, + + #[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-rust/src/events.rs b/crates/runc-rust/src/events.rs new file mode 100644 index 0000000..9bf7125 --- /dev/null +++ b/crates/runc-rust/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-rust/src/io.rs b/crates/runc-rust/src/io.rs new file mode 100644 index 0000000..332aaf9 --- /dev/null +++ b/crates/runc-rust/src/io.rs @@ -0,0 +1,333 @@ +/* + 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, RawFd}; +use std::process::Command; +use std::sync::Mutex; + +use nix::fcntl::OFlag; +use nix::sys::stat::Mode; +use nix::unistd::{Gid, Uid}; + +pub trait RuncIO: 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 + } + + fn close(&self) { + unimplemented!() + } + + /// 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<()> { + unimplemented!() + } + + // tokio version of set() + fn set_tk(&self, _cmd: &mut tokio::process::Command) -> std::io::Result<()> { + unimplemented!() + } + + fn close_after_start(&self) { + unimplemented!() + } +} + +impl Debug for dyn RuncIO { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "RuncIO",) + } +} + +#[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(); + } + + pub fn close(&self) { + self.close_read(); + self.close_write(); + } +} + +#[derive(Debug)] +pub struct RuncPipedIO { + stdin: Option, + stdout: Option, + stderr: Option, +} + +impl RuncPipedIO { + pub fn new(uid: isize, gid: isize, opts: IOOption) -> std::io::Result { + let uid = Some(Uid::from_raw(uid as u32)); + let gid = Some(Gid::from_raw(gid as u32)); + 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 RuncIO for RuncPipedIO { + 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 + } + } + + fn close(&self) { + if let Some(ref stdin) = self.stdin { + stdin.close(); + } + if let Some(ref stdout) = self.stdout { + stdout.close(); + } + if let Some(ref stderr) = self.stderr { + stderr.close(); + } + } + + /// 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.stdin(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.stdin(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.stdin(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.stdin(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: RawFd, +} + +impl NullIO { + pub fn new() -> std::io::Result { + let fd = nix::fcntl::open("/dev/null", OFlag::O_RDONLY, Mode::empty())?; + Ok(Self { dev_null: fd }) + } +} + +impl RuncIO for NullIO { + fn set(&self, cmd: &mut Command) -> std::io::Result<()> { + let null = unsafe { std::fs::File::from_raw_fd(self.dev_null) }; + cmd.stdout(null.try_clone()?); + cmd.stderr(null.try_clone()?); + std::mem::forget(null); + Ok(()) + } + + fn set_tk(&self, cmd: &mut tokio::process::Command) -> std::io::Result<()> { + let null = unsafe { std::fs::File::from_raw_fd(self.dev_null) }; + cmd.stdout(null.try_clone()?); + cmd.stderr(null.try_clone()?); + std::mem::forget(null); + Ok(()) + } + + fn close(&self) { + let _ = unsafe { std::fs::File::from_raw_fd(self.dev_null) }; + } + + fn close_after_start(&self) { + self.close(); + } +} diff --git a/crates/runc-rust/src/lib.rs b/crates/runc-rust/src/lib.rs new file mode 100644 index 0000000..5f3d6d3 --- /dev/null +++ b/crates/runc-rust/src/lib.rs @@ -0,0 +1,1009 @@ +/* + 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::io::Write; +use std::path::Path; +use std::process::{ExitStatus, Output, Stdio}; +use std::time::Duration; + +use tempfile::NamedTempFile; +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; + +/// RuncResponse is for (pid, exit status, outputs). +#[derive(Debug, Clone)] +pub struct RuncResponse { + 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 RuncConfig directly. +/// +/// # Example +/// +/// ```no_run +/// use containerd_runc_rust as runc; +/// +/// let config = runc::RuncConfig::new() +/// .root("./new_root") +/// .debug(false) +/// .log("/path/to/logfile.json") +/// .log_format(runc::LogFormat::Json) +/// .rootless(true); +/// let client = config.build(); +/// ``` +#[derive(Debug, Clone, Default)] +pub struct RuncConfig(runc::RuncConfig); + +impl RuncConfig { + 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(RuncClient(self.0.build()?)) + } + + pub fn build_async(&mut self) -> Result { + Ok(RuncAsyncClient(self.0.build()?)) + } +} + +#[derive(Debug, Clone)] +pub struct RuncClient(runc::Runc); + +impl RuncClient { + /// Create a new runc client from the supplied configuration + pub fn from_config(mut config: RuncConfig) -> 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, + forget: 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 forget { + // reserve fds of pipes for after use + std::mem::forget(cmd); + } + if status.success() { + if combined_output { + Ok(RuncResponse { + pid, + status, + output: stdout + stderr.as_str(), + }) + } else { + Ok(RuncResponse { + 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, true)?; + _io.close_after_start(); + Ok(res) + } + _ => self.launch(cmd, true, false), + } + } + + /// 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, false)?; + Ok(()) + } + + /// Execute an additional process inside the container + pub fn exec(&self, id: &str, spec: &Process, opts: Option<&ExecOpts>) -> Result<()> { + let (mut temp_file, file_name): (NamedTempFile, String) = + utils::make_temp_file_in_runtime_dir()?; + { + let f = temp_file.as_file_mut(); + let spec_json = + serde_json::to_string(spec).map_err(Error::JsonDeserializationFailed)?; + f.write(spec_json.as_bytes()) + .map_err(Error::SpecFileCreationError)?; + f.flush().map_err(Error::SpecFileCreationError)?; + } + let mut args = vec!["exec".to_string(), "process".to_string(), file_name]; + if let Some(opts) = opts { + args.append(&mut opts.args()?); + } + args.push(id.to_string()); + let mut cmd = self.command(&args)?; + let forget = match opts { + Some(ExecOpts { io: Some(_io), .. }) => { + _io.set(&mut cmd).map_err(Error::UnavailableIO)?; + true + } + _ => false, + }; + let _ = self.launch(cmd, true, forget)?; + 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, false)?; + 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, false)?; + 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, false)?; + 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, false)?; + 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)?; + let forget = match opts { + Some(CreateOpts { io: Some(_io), .. }) => { + _io.set(&mut cmd).map_err(Error::UnavailableIO)?; + true + } + _ => false, + }; + + self.launch(self.command(&args)?, true, forget) + } + + /// 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, false) + } + + /// 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, false)?; + Ok(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 (mut temp_file, file_name): (NamedTempFile, String) = + utils::make_temp_file_in_runtime_dir()?; + { + let f = temp_file.as_file_mut(); + let spec_json = + serde_json::to_string(resources).map_err(Error::JsonDeserializationFailed)?; + f.write(spec_json.as_bytes()) + .map_err(Error::SpecFileCreationError)?; + f.flush().map_err(Error::SpecFileCreationError)?; + } + let args = [ + "update".to_string(), + "--resources".to_string(), + file_name, + id.to_string(), + ]; + self.launch(self.command(&args)?, true, false)?; + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub struct RuncAsyncClient(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 RuncAsyncClient { + /// Create a new runc client from the supplied configuration + pub fn from_config(mut config: RuncConfig) -> 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, + forget: bool, + ) -> Result { + let (tx, rx) = tokio::sync::oneshot::channel::(); + let start = MONITOR.start(cmd, tx, forget); + 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(RuncResponse { + pid, + status, + output: stdout + stderr.as_str(), + }) + } else { + Ok(RuncResponse { + 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, true); + let wait = MONITOR.wait(rx); + _io.close_after_start(); + let ( + Output { + status, + stdout, + stderr, + }, + _, + ) = tokio::try_join!(start, wait).map_err(Error::InvalidCommand)?; + + 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, false).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, false).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, false).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, false).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, false).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, false).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, false).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, false).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, false).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, false).await?; + Ok(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, false).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 (mut temp_file, file_name): (NamedTempFile, String) = + utils::make_temp_file_in_runtime_dir()?; + { + let f = temp_file.as_file_mut(); + let spec_json = + serde_json::to_string(resources).map_err(Error::JsonDeserializationFailed)?; + f.write(spec_json.as_bytes()) + .map_err(Error::SpecFileCreationError)?; + f.flush().map_err(Error::SpecFileCreationError)?; + } + let args = vec![ + "update".to_string(), + "--resources".to_string(), + file_name, + id.to_string(), + ]; + let _ = self.launch(self.command(&args)?, true, false).await?; + Ok(()) + } +} + +#[cfg(test)] +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() -> RuncClient { + RuncConfig::new() + .command(CMD_TRUE) + .build() + .expect("unable to create runc instance") + } + + fn fail_client() -> RuncClient { + RuncConfig::new() + .command(CMD_FALSE) + .build() + .expect("unable to create runc instance") + } + + fn ok_async_client() -> RuncAsyncClient { + RuncConfig::new() + .command(CMD_TRUE) + .build_async() + .expect("unable to create runc instance") + } + + fn fail_async_client() -> RuncAsyncClient { + RuncConfig::new() + .command(CMD_FALSE) + .build_async() + .expect("unable to create runc instance") + } + + fn dummy_process() -> Process { + serde_json::from_str("{}").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 = RuncConfig::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 = RuncConfig::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 = RuncConfig::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 = RuncConfig::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-rust/src/monitor.rs b/crates/runc-rust/src/monitor.rs new file mode 100644 index 0000000..04ec647 --- /dev/null +++ b/crates/runc-rust/src/monitor.rs @@ -0,0 +1,86 @@ +/* + 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 chrono::{DateTime, Utc}; +use log::error; +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, + forget: bool, + ) -> 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 = Utc::now(); + match tx.send(Exit { + ts, + pid, + status: out.status.code().unwrap(), + }) { + Ok(_) => { + if forget { + std::mem::forget(cmd); + } + 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: DateTime, + pub pid: u32, + pub status: i32, +} diff --git a/crates/runc-rust/src/options.rs b/crates/runc-rust/src/options.rs new file mode 100644 index 0000000..b4659eb --- /dev/null +++ b/crates/runc-rust/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::RuncIO; +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-rust/src/runc.rs b/crates/runc-rust/src/runc.rs new file mode 100644 index 0000000..e5fd69d --- /dev/null +++ b/crates/runc-rust/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 RuncConfig { + /// 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 RuncConfig { + 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(PathBuf::from(DEFAULT_COMMAND)), + ) + .ok_or(Error::NotFound)?; + Ok(Runc { + command, + root: self.root.clone(), + debug: self.debug.clone(), + 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(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-rust/src/utils.rs b/crates/runc-rust/src/utils.rs new file mode 100644 index 0000000..252033b --- /dev/null +++ b/crates/runc-rust/src/utils.rs @@ -0,0 +1,102 @@ +/* + 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::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 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_else(|| Error::SpecFileNotFound)?; + let temp_file = Builder::new() + .prefix(&file_name) + .tempfile() + .map_err(Error::SpecFileCreationError)?; + 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/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"] From 7c2b93f18782d647806b4f95051620e89debeb3a Mon Sep 17 00:00:00 2001 From: Yuna Tomida Date: Thu, 3 Feb 2022 04:03:49 +0000 Subject: [PATCH 02/10] removed chrono from dependencies and applied fmt Signed-off-by: Yuna Tomida --- crates/runc-rust/Cargo.toml | 2 +- crates/runc-rust/src/container.rs | 30 ++++++++++++++++++++++++++---- crates/runc-rust/src/lib.rs | 2 +- crates/runc-rust/src/monitor.rs | 6 +++--- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/crates/runc-rust/Cargo.toml b/crates/runc-rust/Cargo.toml index e624c2f..6bd72b0 100644 --- a/crates/runc-rust/Cargo.toml +++ b/crates/runc-rust/Cargo.toml @@ -13,7 +13,6 @@ 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" -chrono = { version = "0.4.19", features = ["serde"] } futures = "0.3.19" libc = "0.2.112" log = "0.4.14" @@ -25,5 +24,6 @@ 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-rust/src/container.rs b/crates/runc-rust/src/container.rs index aab8b6b..1f1dcca 100644 --- a/crates/runc-rust/src/container.rs +++ b/crates/runc-rust/src/container.rs @@ -35,9 +35,8 @@ use std::collections::HashMap; -use chrono::serde::ts_seconds_option; -use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; /// Information for runc container #[derive(Debug, Serialize, Deserialize)] @@ -47,7 +46,30 @@ pub struct Container { pub status: String, pub bundle: String, pub rootfs: String, - #[serde(with = "ts_seconds_option")] - pub created: Option>, + pub created: OffsetDateTime, pub annotations: HashMap, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde_test() { + let j = " + { + \"id\": \"fake\", + \"pid\": 1000, + \"status\": \"RUNNING\", + \"bundle\": \"/path/to/root\", + \"rootfs\": \"/path/to/rootfs\", + \"created\": 1431684000, + \"annotations\": { + \"foo\": \"bar\" + } + }"; + + let c: Container = serde_json::from_str(j).unwrap(); + println!("{:#?}", c); + } +} diff --git a/crates/runc-rust/src/lib.rs b/crates/runc-rust/src/lib.rs index 5f3d6d3..7001d3c 100644 --- a/crates/runc-rust/src/lib.rs +++ b/crates/runc-rust/src/lib.rs @@ -41,8 +41,8 @@ use std::path::Path; use std::process::{ExitStatus, Output, Stdio}; use std::time::Duration; -use tempfile::NamedTempFile; use oci_spec::runtime::{Linux, Process}; +use tempfile::NamedTempFile; // suspended for difficulties // pub mod console; diff --git a/crates/runc-rust/src/monitor.rs b/crates/runc-rust/src/monitor.rs index 04ec647..4bacb6d 100644 --- a/crates/runc-rust/src/monitor.rs +++ b/crates/runc-rust/src/monitor.rs @@ -17,8 +17,8 @@ use std::process::Output; use async_trait::async_trait; -use chrono::{DateTime, Utc}; use log::error; +use time::OffsetDateTime; use tokio::sync::oneshot::{Receiver, Sender}; // ProcessMonitor for handling runc process exit @@ -40,7 +40,7 @@ pub trait ProcessMonitor { .id() .expect("failed to take pid of the container process."); let out = chi.wait_with_output().await?; - let ts = Utc::now(); + let ts = OffsetDateTime::now_utc(); match tx.send(Exit { ts, pid, @@ -80,7 +80,7 @@ impl DefaultMonitor { #[derive(Debug)] pub struct Exit { - pub ts: DateTime, + pub ts: OffsetDateTime, pub pid: u32, pub status: i32, } From 2663849a5293b8b8085705f3f79543712d77891d Mon Sep 17 00:00:00 2001 From: Yuna Tomida Date: Thu, 3 Feb 2022 07:04:26 +0000 Subject: [PATCH 03/10] confirmed tests Signed-off-by: Yuna Tomida --- crates/runc-rust/src/container.rs | 2 ++ crates/runc-rust/src/lib.rs | 24 +++++++++++++++++------- crates/runc-rust/src/runc.rs | 6 +++--- crates/runc-rust/src/utils.rs | 2 +- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/crates/runc-rust/src/container.rs b/crates/runc-rust/src/container.rs index 1f1dcca..9215770 100644 --- a/crates/runc-rust/src/container.rs +++ b/crates/runc-rust/src/container.rs @@ -36,6 +36,7 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; +use time::serde::timestamp; use time::OffsetDateTime; /// Information for runc container @@ -46,6 +47,7 @@ pub struct Container { pub status: String, pub bundle: String, pub rootfs: String, + #[serde(with = "timestamp")] pub created: OffsetDateTime, pub annotations: HashMap, } diff --git a/crates/runc-rust/src/lib.rs b/crates/runc-rust/src/lib.rs index 7001d3c..3580b6c 100644 --- a/crates/runc-rust/src/lib.rs +++ b/crates/runc-rust/src/lib.rs @@ -101,14 +101,14 @@ impl Display for LogFormat { /// /// # Example /// -/// ```no_run -/// use containerd_runc_rust as runc; -/// -/// let config = runc::RuncConfig::new() +/// ```ignore +/// use runc::{LogFormat, RuncConfig}; +/// +/// let config = RuncConfig::new() /// .root("./new_root") /// .debug(false) /// .log("/path/to/logfile.json") -/// .log_format(runc::LogFormat::Json) +/// .log_format(LogFormat::Json) /// .rootless(true); /// let client = config.build(); /// ``` @@ -411,7 +411,7 @@ impl RuncClient { pub fn state(&self, id: &str) -> Result { let args = ["state".to_string(), id.to_string()]; let res = self.launch(self.command(&args)?, true, false)?; - Ok(serde_json::from_str(&res.output).map_err(Error::JsonDeserializationFailed)?) + serde_json::from_str(&res.output).map_err(Error::JsonDeserializationFailed) } /// Update a container with the provided resource spec @@ -744,7 +744,17 @@ mod tests { } fn dummy_process() -> Process { - serde_json::from_str("{}").unwrap() + serde_json::from_str( + " + { + \"user\": { + \"uid\": 1000, + \"gid\": 1000 + }, + \"cwd\": \"/path/to/dir\" + }", + ) + .unwrap() } #[test] diff --git a/crates/runc-rust/src/runc.rs b/crates/runc-rust/src/runc.rs index e5fd69d..b39ca2d 100644 --- a/crates/runc-rust/src/runc.rs +++ b/crates/runc-rust/src/runc.rs @@ -138,13 +138,13 @@ impl RuncConfig { let command = utils::binary_path( self.command .clone() - .unwrap_or(PathBuf::from(DEFAULT_COMMAND)), + .unwrap_or_else(|| PathBuf::from(DEFAULT_COMMAND)), ) .ok_or(Error::NotFound)?; Ok(Runc { command, root: self.root.clone(), - debug: self.debug.clone(), + debug: self.debug, log: self.log.clone(), log_format: self.log_format.clone().unwrap_or(LogFormat::Text), // self.pdeath_signal: self.pdeath_signal, @@ -153,7 +153,7 @@ impl RuncConfig { // criu: self.criu, rootless: self.rootless, // extra_args: self.extra_args, - timeout: self.timeout.unwrap_or(Duration::from_millis(5000)), + timeout: self.timeout.unwrap_or_else(|| Duration::from_millis(5000)), }) } } diff --git a/crates/runc-rust/src/utils.rs b/crates/runc-rust/src/utils.rs index 252033b..a589c65 100644 --- a/crates/runc-rust/src/utils.rs +++ b/crates/runc-rust/src/utils.rs @@ -77,7 +77,7 @@ pub fn make_temp_file_in_runtime_dir() -> Result<(NamedTempFile, String), Error> Uuid::new_v4(), ) }) - .ok_or_else(|| Error::SpecFileNotFound)?; + .ok_or(Error::SpecFileNotFound)?; let temp_file = Builder::new() .prefix(&file_name) .tempfile() From 43cf334648b3c9f753ce23fe62d1c1be8ad389d3 Mon Sep 17 00:00:00 2001 From: Yuna Tomida Date: Thu, 3 Feb 2022 08:35:04 +0000 Subject: [PATCH 04/10] applied fmt Signed-off-by: Yuna Tomida --- crates/runc-rust/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/runc-rust/src/lib.rs b/crates/runc-rust/src/lib.rs index 3580b6c..8ece8da 100644 --- a/crates/runc-rust/src/lib.rs +++ b/crates/runc-rust/src/lib.rs @@ -103,7 +103,7 @@ impl Display for LogFormat { /// /// ```ignore /// use runc::{LogFormat, RuncConfig}; -/// +/// /// let config = RuncConfig::new() /// .root("./new_root") /// .debug(false) From 8bbd4e7baf1a45517f92a359fe2db244b65daa6f Mon Sep 17 00:00:00 2001 From: Yuna Tomida Date: Thu, 3 Feb 2022 13:35:24 +0000 Subject: [PATCH 05/10] fixed clippy warnings in shim::args and example in client Signed-off-by: Yuna Tomida --- crates/client/examples/container.rs | 2 +- crates/shim/src/args.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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/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) { From 8f3e556a1b37622d2a9d002830d03eea9d079a75 Mon Sep 17 00:00:00 2001 From: Yuna Tomida Date: Thu, 3 Feb 2022 17:12:18 +0000 Subject: [PATCH 06/10] renamed crate and applied recommended changes Signed-off-by: Yuna Tomida --- Cargo.toml | 2 +- crates/{runc-rust => runc}/Cargo.toml | 0 crates/{runc-rust => runc}/README.md | 6 +- crates/{runc-rust => runc}/src/container.rs | 32 +++-- crates/{runc-rust => runc}/src/error.rs | 8 +- crates/{runc-rust => runc}/src/events.rs | 0 crates/{runc-rust => runc}/src/io.rs | 96 ++++++--------- crates/{runc-rust => runc}/src/lib.rs | 129 ++++++++------------ crates/{runc-rust => runc}/src/monitor.rs | 0 crates/{runc-rust => runc}/src/options.rs | 10 +- crates/{runc-rust => runc}/src/runc.rs | 4 +- crates/{runc-rust => runc}/src/utils.rs | 16 ++- 12 files changed, 139 insertions(+), 164 deletions(-) rename crates/{runc-rust => runc}/Cargo.toml (100%) rename crates/{runc-rust => runc}/README.md (91%) rename crates/{runc-rust => runc}/src/container.rs (71%) rename crates/{runc-rust => runc}/src/error.rs (95%) rename crates/{runc-rust => runc}/src/events.rs (100%) rename crates/{runc-rust => runc}/src/io.rs (79%) rename crates/{runc-rust => runc}/src/lib.rs (90%) rename crates/{runc-rust => runc}/src/monitor.rs (100%) rename crates/{runc-rust => runc}/src/options.rs (97%) rename crates/{runc-rust => runc}/src/runc.rs (99%) rename crates/{runc-rust => runc}/src/utils.rs (86%) diff --git a/Cargo.toml b/Cargo.toml index ec5bed1..6741814 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,5 +5,5 @@ members = [ "crates/shim-protos", "crates/shim", "crates/snapshots", - "crates/runc-rust" + "crates/runc" ] diff --git a/crates/runc-rust/Cargo.toml b/crates/runc/Cargo.toml similarity index 100% rename from crates/runc-rust/Cargo.toml rename to crates/runc/Cargo.toml diff --git a/crates/runc-rust/README.md b/crates/runc/README.md similarity index 91% rename from crates/runc-rust/README.md rename to crates/runc/README.md index b372f3c..77a43a2 100644 --- a/crates/runc-rust/README.md +++ b/crates/runc/README.md @@ -10,11 +10,11 @@ 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_rust as runc; +use runc; #[tokio::main] async fn main() { - let config = runc::RuncConfig::new() + let config = runc::Config::new() .root("./new_root") .debug(false) .log("/path/to/logfile.json") @@ -23,7 +23,7 @@ async fn main() { let client = config.build_async().unwrap(); - let opts = CreateOpts::new() + let opts = runc::options::CreateOpts::new() .pid_file("/path/to/pid/file") .no_pivot(true); diff --git a/crates/runc-rust/src/container.rs b/crates/runc/src/container.rs similarity index 71% rename from crates/runc-rust/src/container.rs rename to crates/runc/src/container.rs index 9215770..1351ca2 100644 --- a/crates/runc-rust/src/container.rs +++ b/crates/runc/src/container.rs @@ -58,20 +58,30 @@ mod tests { #[test] fn serde_test() { - let j = " + let j = r#" { - \"id\": \"fake\", - \"pid\": 1000, - \"status\": \"RUNNING\", - \"bundle\": \"/path/to/root\", - \"rootfs\": \"/path/to/rootfs\", - \"created\": 1431684000, - \"annotations\": { - \"foo\": \"bar\" + "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(); - println!("{:#?}", c); + 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-rust/src/error.rs b/crates/runc/src/error.rs similarity index 95% rename from crates/runc-rust/src/error.rs rename to crates/runc/src/error.rs index 62d23ce..65b1aa6 100644 --- a/crates/runc-rust/src/error.rs +++ b/crates/runc/src/error.rs @@ -82,10 +82,10 @@ pub enum Error { FileSystemError(io::Error), #[error("Failed to spec file: {0}")] - SpecFileCreationError(io::Error), + SpecFileCreationFailed(io::Error), #[error(transparent)] - SpecFileCleanupError(io::Error), + SpecFileCleanupFailed(io::Error), #[error("Failed to find valid path for spec file")] SpecFileNotFound, @@ -103,10 +103,10 @@ pub enum Error { UnixSocketBindFailed(io::Error), #[error("Unix socket failed to receive pty")] - UnixSocketReceiveMessageError, + UnixSocketReceiveMessageFailed, #[error("Unix socket unexpectedly closed")] - UnixSocketUnexpectedCloseError, + UnixSocketClosed, #[error("Failed to handle environment variable: {0}")] EnvError(env::VarError), diff --git a/crates/runc-rust/src/events.rs b/crates/runc/src/events.rs similarity index 100% rename from crates/runc-rust/src/events.rs rename to crates/runc/src/events.rs diff --git a/crates/runc-rust/src/io.rs b/crates/runc/src/io.rs similarity index 79% rename from crates/runc-rust/src/io.rs rename to crates/runc/src/io.rs index 332aaf9..3d4364b 100644 --- a/crates/runc-rust/src/io.rs +++ b/crates/runc/src/io.rs @@ -16,7 +16,7 @@ use std::fmt::{self, Debug, Formatter}; use std::fs::File; use std::os::unix::io::FromRawFd; -use std::os::unix::prelude::{AsRawFd, RawFd}; +use std::os::unix::prelude::AsRawFd; use std::process::Command; use std::sync::Mutex; @@ -24,7 +24,7 @@ use nix::fcntl::OFlag; use nix::sys::stat::Mode; use nix::unistd::{Gid, Uid}; -pub trait RuncIO: Sync + Send { +pub trait Io: Sync + Send { /// Return write side of stdin fn stdin(&self) -> Option { None @@ -40,29 +40,19 @@ pub trait RuncIO: Sync + Send { None } - fn close(&self) { - unimplemented!() - } - /// 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<()> { - unimplemented!() - } + 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<()> { - unimplemented!() - } + fn set_tk(&self, _cmd: &mut tokio::process::Command) -> std::io::Result<()>; - fn close_after_start(&self) { - unimplemented!() - } + fn close_after_start(&self); } -impl Debug for dyn RuncIO { +impl Debug for dyn Io { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "RuncIO",) + write!(f, "Io",) } } @@ -124,24 +114,19 @@ impl Pipe { let mut m = self.wr.lock().unwrap(); let _ = m.take(); } - - pub fn close(&self) { - self.close_read(); - self.close_write(); - } } #[derive(Debug)] -pub struct RuncPipedIO { +pub struct PipedIo { stdin: Option, stdout: Option, stderr: Option, } -impl RuncPipedIO { - pub fn new(uid: isize, gid: isize, opts: IOOption) -> std::io::Result { - let uid = Some(Uid::from_raw(uid as u32)); - let gid = Some(Gid::from_raw(gid as u32)); +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()?; { @@ -189,7 +174,7 @@ impl RuncPipedIO { } } -impl RuncIO for RuncPipedIO { +impl Io for PipedIo { fn stdin(&self) -> Option { if let Some(ref stdin) = self.stdin { stdin.take_write() @@ -214,18 +199,6 @@ impl RuncIO for RuncPipedIO { } } - fn close(&self) { - if let Some(ref stdin) = self.stdin { - stdin.close(); - } - if let Some(ref stdout) = self.stdout { - stdout.close(); - } - if let Some(ref stderr) = self.stderr { - stderr.close(); - } - } - /// 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<()> { @@ -241,7 +214,7 @@ impl RuncIO for RuncPipedIO { let m = p.wr.lock().unwrap(); if let Some(f) = &*m { let f = f.try_clone()?; - cmd.stdin(f); + cmd.stdout(f); } } @@ -249,7 +222,7 @@ impl RuncIO for RuncPipedIO { let m = p.wr.lock().unwrap(); if let Some(f) = &*m { let f = f.try_clone()?; - cmd.stdin(f); + cmd.stderr(f); } } Ok(()) @@ -268,7 +241,7 @@ impl RuncIO for RuncPipedIO { let m = p.wr.lock().unwrap(); if let Some(f) = &*m { let f = f.try_clone()?; - cmd.stdin(f); + cmd.stdout(f); } } @@ -276,7 +249,7 @@ impl RuncIO for RuncPipedIO { let m = p.wr.lock().unwrap(); if let Some(f) = &*m { let f = f.try_clone()?; - cmd.stdin(f); + cmd.stderr(f); } } Ok(()) @@ -295,39 +268,38 @@ impl RuncIO for RuncPipedIO { // IO setup for /dev/null use with runc #[derive(Debug)] -pub struct NullIO { - dev_null: RawFd, +pub struct NullIo { + dev_null: Mutex>, } -impl NullIO { +impl NullIo { pub fn new() -> std::io::Result { let fd = nix::fcntl::open("/dev/null", OFlag::O_RDONLY, Mode::empty())?; - Ok(Self { dev_null: fd }) + let dev_null = unsafe { Mutex::new(Some(std::fs::File::from_raw_fd(fd))) }; + Ok(Self { dev_null }) } } -impl RuncIO for NullIO { +impl Io for NullIo { fn set(&self, cmd: &mut Command) -> std::io::Result<()> { - let null = unsafe { std::fs::File::from_raw_fd(self.dev_null) }; - cmd.stdout(null.try_clone()?); - cmd.stderr(null.try_clone()?); - std::mem::forget(null); + 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<()> { - let null = unsafe { std::fs::File::from_raw_fd(self.dev_null) }; - cmd.stdout(null.try_clone()?); - cmd.stderr(null.try_clone()?); - std::mem::forget(null); + if let Some(null) = self.dev_null.lock().unwrap().as_ref() { + cmd.stdout(null.try_clone()?); + cmd.stderr(null.try_clone()?); + } Ok(()) } - fn close(&self) { - let _ = unsafe { std::fs::File::from_raw_fd(self.dev_null) }; - } - + /// closing only write side (should be stdout/err "from" runc process) fn close_after_start(&self) { - self.close(); + let mut m = self.dev_null.lock().unwrap(); + let _ = m.take(); } } diff --git a/crates/runc-rust/src/lib.rs b/crates/runc/src/lib.rs similarity index 90% rename from crates/runc-rust/src/lib.rs rename to crates/runc/src/lib.rs index 8ece8da..1c05f89 100644 --- a/crates/runc-rust/src/lib.rs +++ b/crates/runc/src/lib.rs @@ -36,13 +36,11 @@ //! 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::io::Write; use std::path::Path; use std::process::{ExitStatus, Output, Stdio}; use std::time::Duration; use oci_spec::runtime::{Linux, Process}; -use tempfile::NamedTempFile; // suspended for difficulties // pub mod console; @@ -64,9 +62,9 @@ use crate::utils::{JSON, TEXT}; type Result = std::result::Result; -/// RuncResponse is for (pid, exit status, outputs). +/// Response is for (pid, exit status, outputs). #[derive(Debug, Clone)] -pub struct RuncResponse { +pub struct Response { pub pid: u32, pub status: ExitStatus, pub output: String, @@ -97,14 +95,14 @@ impl Display for LogFormat { /// Configuration for runc client. /// /// This struct provide chaining interface like, for example, [`std::fs::OpenOptions`]. -/// Note that you cannot access the members of RuncConfig directly. +/// Note that you cannot access the members of Config directly. /// /// # Example /// /// ```ignore -/// use runc::{LogFormat, RuncConfig}; +/// use runc::{LogFormat, Config}; /// -/// let config = RuncConfig::new() +/// let config = Config::new() /// .root("./new_root") /// .debug(false) /// .log("/path/to/logfile.json") @@ -113,9 +111,9 @@ impl Display for LogFormat { /// let client = config.build(); /// ``` #[derive(Debug, Clone, Default)] -pub struct RuncConfig(runc::RuncConfig); +pub struct Config(runc::Config); -impl RuncConfig { +impl Config { pub fn new() -> Self { Self::default() } @@ -195,21 +193,21 @@ impl RuncConfig { self } - pub fn build(&mut self) -> Result { - Ok(RuncClient(self.0.build()?)) + pub fn build(&mut self) -> Result { + Ok(Client(self.0.build()?)) } - pub fn build_async(&mut self) -> Result { - Ok(RuncAsyncClient(self.0.build()?)) + pub fn build_async(&mut self) -> Result { + Ok(AsyncClient(self.0.build()?)) } } #[derive(Debug, Clone)] -pub struct RuncClient(runc::Runc); +pub struct Client(runc::Runc); -impl RuncClient { +impl Client { /// Create a new runc client from the supplied configuration - pub fn from_config(mut config: RuncConfig) -> Result { + pub fn from_config(mut config: Config) -> Result { config.build() } @@ -235,7 +233,7 @@ impl RuncClient { mut cmd: std::process::Command, combined_output: bool, forget: bool, - ) -> Result { + ) -> Result { let child = cmd.spawn().map_err(Error::ProcessSpawnFailed)?; let pid = child.id(); let result = child.wait_with_output().map_err(Error::InvalidCommand)?; @@ -248,13 +246,13 @@ impl RuncClient { } if status.success() { if combined_output { - Ok(RuncResponse { + Ok(Response { pid, status, output: stdout + stderr.as_str(), }) } else { - Ok(RuncResponse { + Ok(Response { pid, status, output: stdout, @@ -270,7 +268,7 @@ impl RuncClient { } /// Create a new container - pub fn create

(&self, id: &str, bundle: P, opts: Option<&CreateOpts>) -> Result + pub fn create

(&self, id: &str, bundle: P, opts: Option<&CreateOpts>) -> Result where P: AsRef, { @@ -308,17 +306,10 @@ impl RuncClient { /// Execute an additional process inside the container pub fn exec(&self, id: &str, spec: &Process, opts: Option<&ExecOpts>) -> Result<()> { - let (mut temp_file, file_name): (NamedTempFile, String) = - utils::make_temp_file_in_runtime_dir()?; - { - let f = temp_file.as_file_mut(); - let spec_json = - serde_json::to_string(spec).map_err(Error::JsonDeserializationFailed)?; - f.write(spec_json.as_bytes()) - .map_err(Error::SpecFileCreationError)?; - f.flush().map_err(Error::SpecFileCreationError)?; - } - let mut args = vec!["exec".to_string(), "process".to_string(), file_name]; + 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()?); } @@ -379,7 +370,7 @@ impl RuncClient { } /// 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 + pub fn run

(&self, id: &str, bundle: P, opts: Option<&CreateOpts>) -> Result where P: AsRef, { @@ -402,7 +393,7 @@ impl RuncClient { } /// Start an already created container - pub fn start(&self, id: &str) -> Result { + pub fn start(&self, id: &str) -> Result { let args = ["start".to_string(), id.to_string()]; self.launch(self.command(&args)?, true, false) } @@ -416,20 +407,14 @@ impl RuncClient { /// Update a container with the provided resource spec pub fn update(&self, id: &str, resources: &Linux) -> Result<()> { - let (mut temp_file, file_name): (NamedTempFile, String) = - utils::make_temp_file_in_runtime_dir()?; - { - let f = temp_file.as_file_mut(); - let spec_json = - serde_json::to_string(resources).map_err(Error::JsonDeserializationFailed)?; - f.write(spec_json.as_bytes()) - .map_err(Error::SpecFileCreationError)?; - f.flush().map_err(Error::SpecFileCreationError)?; - } + 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(), - file_name, + filename, id.to_string(), ]; self.launch(self.command(&args)?, true, false)?; @@ -438,7 +423,7 @@ impl RuncClient { } #[derive(Debug, Clone)] -pub struct RuncAsyncClient(runc::Runc); +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(); @@ -446,9 +431,9 @@ 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 RuncAsyncClient { +impl AsyncClient { /// Create a new runc client from the supplied configuration - pub fn from_config(mut config: RuncConfig) -> Result { + pub fn from_config(mut config: Config) -> Result { config.build_async() } @@ -467,7 +452,7 @@ impl RuncAsyncClient { cmd: tokio::process::Command, combined_output: bool, forget: bool, - ) -> Result { + ) -> Result { let (tx, rx) = tokio::sync::oneshot::channel::(); let start = MONITOR.start(cmd, tx, forget); let wait = MONITOR.wait(rx); @@ -488,13 +473,13 @@ impl RuncAsyncClient { if status.success() { if combined_output { - Ok(RuncResponse { + Ok(Response { pid, status, output: stdout + stderr.as_str(), }) } else { - Ok(RuncResponse { + Ok(Response { pid, status, output: stdout, @@ -534,7 +519,6 @@ impl RuncAsyncClient { let (tx, rx) = tokio::sync::oneshot::channel::(); let start = MONITOR.start(cmd, tx, true); let wait = MONITOR.wait(rx); - _io.close_after_start(); let ( Output { status, @@ -543,6 +527,7 @@ impl RuncAsyncClient { }, _, ) = 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(); @@ -668,7 +653,7 @@ impl RuncAsyncClient { 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, false).await?; - Ok(serde_json::from_str(&res.output).map_err(Error::JsonDeserializationFailed)?) + serde_json::from_str(&res.output).map_err(Error::JsonDeserializationFailed) } /// Return the latest statistics for a container @@ -686,20 +671,14 @@ impl RuncAsyncClient { /// Update a container with the provided resource spec pub async fn update(&self, id: &str, resources: &Linux) -> Result<()> { - let (mut temp_file, file_name): (NamedTempFile, String) = - utils::make_temp_file_in_runtime_dir()?; - { - let f = temp_file.as_file_mut(); - let spec_json = - serde_json::to_string(resources).map_err(Error::JsonDeserializationFailed)?; - f.write(spec_json.as_bytes()) - .map_err(Error::SpecFileCreationError)?; - f.flush().map_err(Error::SpecFileCreationError)?; - } + 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(), - file_name, + filename, id.to_string(), ]; let _ = self.launch(self.command(&args)?, true, false).await?; @@ -715,29 +694,29 @@ mod tests { const CMD_TRUE: &str = "/bin/true"; const CMD_FALSE: &str = "/bin/false"; - fn ok_client() -> RuncClient { - RuncConfig::new() + fn ok_client() -> Client { + Config::new() .command(CMD_TRUE) .build() .expect("unable to create runc instance") } - fn fail_client() -> RuncClient { - RuncConfig::new() + fn fail_client() -> Client { + Config::new() .command(CMD_FALSE) .build() .expect("unable to create runc instance") } - fn ok_async_client() -> RuncAsyncClient { - RuncConfig::new() + fn ok_async_client() -> AsyncClient { + Config::new() .command(CMD_TRUE) .build_async() .expect("unable to create runc instance") } - fn fail_async_client() -> RuncAsyncClient { - RuncConfig::new() + fn fail_async_client() -> AsyncClient { + Config::new() .command(CMD_FALSE) .build_async() .expect("unable to create runc instance") @@ -935,7 +914,7 @@ mod tests { #[tokio::test] async fn test_async_run() { let opts = CreateOpts::new(); - let ok_runc = RuncConfig::new() + let ok_runc = Config::new() .command(CMD_TRUE) .build_async() .expect("unable to create runc instance"); @@ -948,7 +927,7 @@ mod tests { }); let opts = CreateOpts::new(); - let fail_runc = RuncConfig::new() + let fail_runc = Config::new() .command(CMD_FALSE) .build_async() .expect("unable to create runc instance"); @@ -979,7 +958,7 @@ mod tests { #[tokio::test] async fn test_async_delete() { let opts = DeleteOpts::new(); - let ok_runc = RuncConfig::new() + let ok_runc = Config::new() .command(CMD_TRUE) .build_async() .expect("unable to create runc instance"); @@ -992,7 +971,7 @@ mod tests { }); let opts = DeleteOpts::new(); - let fail_runc = RuncConfig::new() + let fail_runc = Config::new() .command(CMD_FALSE) .build_async() .expect("unable to create runc instance"); diff --git a/crates/runc-rust/src/monitor.rs b/crates/runc/src/monitor.rs similarity index 100% rename from crates/runc-rust/src/monitor.rs rename to crates/runc/src/monitor.rs diff --git a/crates/runc-rust/src/options.rs b/crates/runc/src/options.rs similarity index 97% rename from crates/runc-rust/src/options.rs rename to crates/runc/src/options.rs index b4659eb..4f3d129 100644 --- a/crates/runc-rust/src/options.rs +++ b/crates/runc/src/options.rs @@ -37,7 +37,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use crate::error::Error; -use crate::io::RuncIO; +use crate::io::Io; use crate::utils::{self, ALL, CONSOLE_SOCKET, DETACH, FORCE, NO_NEW_KEYRING, NO_PIVOT, PID_FILE}; pub trait Args { @@ -47,7 +47,7 @@ pub trait Args { #[derive(Debug, Clone, Default)] pub struct CreateOpts { - pub io: Option>, + 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. @@ -90,7 +90,7 @@ impl CreateOpts { Self::default() } - pub fn io(mut self, io: Arc) -> Self { + pub fn io(mut self, io: Arc) -> Self { self.io = Some(io); self } @@ -130,7 +130,7 @@ impl CreateOpts { /// Container execution options #[derive(Debug, Clone, Default)] pub struct ExecOpts { - pub io: Option>, + 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. @@ -163,7 +163,7 @@ impl ExecOpts { Self::default() } - pub fn io(mut self, io: Arc) -> Self { + pub fn io(mut self, io: Arc) -> Self { self.io = Some(io); self } diff --git a/crates/runc-rust/src/runc.rs b/crates/runc/src/runc.rs similarity index 99% rename from crates/runc-rust/src/runc.rs rename to crates/runc/src/runc.rs index b39ca2d..9f8038e 100644 --- a/crates/runc-rust/src/runc.rs +++ b/crates/runc/src/runc.rs @@ -43,7 +43,7 @@ use crate::LogFormat; /// Inner struct for runc configuration #[derive(Debug, Clone, Default)] -pub struct RuncConfig { +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. @@ -71,7 +71,7 @@ pub struct RuncConfig { timeout: Option, } -impl RuncConfig { +impl Config { pub fn command

(&mut self, command: P) where P: AsRef, diff --git a/crates/runc-rust/src/utils.rs b/crates/runc/src/utils.rs similarity index 86% rename from crates/runc-rust/src/utils.rs rename to crates/runc/src/utils.rs index a589c65..7cee541 100644 --- a/crates/runc-rust/src/utils.rs +++ b/crates/runc/src/utils.rs @@ -14,6 +14,8 @@ limitations under the License. */ +#![allow(unused)] + use std::env; use std::path::{Path, PathBuf}; @@ -68,6 +70,18 @@ where .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| { @@ -81,7 +95,7 @@ pub fn make_temp_file_in_runtime_dir() -> Result<(NamedTempFile, String), Error> let temp_file = Builder::new() .prefix(&file_name) .tempfile() - .map_err(Error::SpecFileCreationError)?; + .map_err(Error::SpecFileCreationFailed)?; Ok((temp_file, file_name)) } From eddd107601149cc0b20056c149be218a755ec242 Mon Sep 17 00:00:00 2001 From: Yuna Tomida Date: Fri, 4 Feb 2022 02:27:54 +0000 Subject: [PATCH 07/10] remove unnecessary 'forget' Signed-off-by: Yuna Tomida --- crates/runc/src/lib.rs | 83 +++++++++++++++----------------------- crates/runc/src/monitor.rs | 8 +--- 2 files changed, 33 insertions(+), 58 deletions(-) diff --git a/crates/runc/src/lib.rs b/crates/runc/src/lib.rs index 1c05f89..403ea58 100644 --- a/crates/runc/src/lib.rs +++ b/crates/runc/src/lib.rs @@ -228,22 +228,13 @@ impl Client { Err(Error::Unimplemented("checkpoint".to_string())) } - fn launch( - &self, - mut cmd: std::process::Command, - combined_output: bool, - forget: bool, - ) -> Result { + 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 forget { - // reserve fds of pipes for after use - std::mem::forget(cmd); - } if status.success() { if combined_output { Ok(Response { @@ -285,11 +276,11 @@ impl Client { match opts { Some(CreateOpts { io: Some(_io), .. }) => { _io.set(&mut cmd).map_err(Error::UnavailableIO)?; - let res = self.launch(cmd, true, true)?; + let res = self.launch(cmd, true)?; _io.close_after_start(); Ok(res) } - _ => self.launch(cmd, true, false), + _ => self.launch(cmd, true), } } @@ -300,7 +291,7 @@ impl Client { args.append(&mut opts.args()); } args.push(id.to_string()); - self.launch(self.command(&args)?, true, false)?; + self.launch(self.command(&args)?, true)?; Ok(()) } @@ -315,14 +306,10 @@ impl Client { } args.push(id.to_string()); let mut cmd = self.command(&args)?; - let forget = match opts { - Some(ExecOpts { io: Some(_io), .. }) => { - _io.set(&mut cmd).map_err(Error::UnavailableIO)?; - true - } - _ => false, - }; - let _ = self.launch(cmd, true, forget)?; + if let Some(ExecOpts { io: Some(_io), .. }) = opts { + _io.set(&mut cmd).map_err(Error::UnavailableIO)?; + } + let _ = self.launch(cmd, true)?; Ok(()) } @@ -334,14 +321,14 @@ impl Client { } args.push(id.to_string()); args.push(sig.to_string()); - let _ = self.launch(self.command(&args)?, true, false)?; + 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, false)?; + let res = self.launch(self.command(&args)?, true)?; let output = res.output.trim(); // Ugly hack to work around golang Ok(if output == "null" { @@ -354,7 +341,7 @@ impl Client { /// 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, false)?; + let _ = self.launch(self.command(&args)?, true)?; Ok(()) } @@ -365,7 +352,7 @@ impl Client { /// 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, false)?; + let _ = self.launch(self.command(&args)?, true)?; Ok(()) } @@ -381,27 +368,22 @@ impl Client { args.push(utils::abs_string(bundle)?); args.push(id.to_string()); let mut cmd = self.command(&args)?; - let forget = match opts { - Some(CreateOpts { io: Some(_io), .. }) => { - _io.set(&mut cmd).map_err(Error::UnavailableIO)?; - true - } - _ => false, + if let Some(CreateOpts { io: Some(_io), .. }) = opts { + _io.set(&mut cmd).map_err(Error::UnavailableIO)?; }; - - self.launch(self.command(&args)?, true, forget) + 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, false) + 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, false)?; + let res = self.launch(self.command(&args)?, true)?; serde_json::from_str(&res.output).map_err(Error::JsonDeserializationFailed) } @@ -417,7 +399,7 @@ impl Client { filename, id.to_string(), ]; - self.launch(self.command(&args)?, true, false)?; + self.launch(self.command(&args)?, true)?; Ok(()) } } @@ -451,10 +433,9 @@ impl AsyncClient { &self, cmd: tokio::process::Command, combined_output: bool, - forget: bool, ) -> Result { let (tx, rx) = tokio::sync::oneshot::channel::(); - let start = MONITOR.start(cmd, tx, forget); + let start = MONITOR.start(cmd, tx); let wait = MONITOR.wait(rx); let ( Output { @@ -517,7 +498,7 @@ impl AsyncClient { 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, true); + let start = MONITOR.start(cmd, tx); let wait = MONITOR.wait(rx); let ( Output { @@ -540,7 +521,7 @@ impl AsyncClient { } } _ => { - let _ = self.launch(cmd, true, false).await?; + let _ = self.launch(cmd, true).await?; } } Ok(()) @@ -553,7 +534,7 @@ impl AsyncClient { args.append(&mut opts.args()); } args.push(id.to_string()); - let _ = self.launch(self.command(&args)?, true, false).await?; + let _ = self.launch(self.command(&args)?, true).await?; Ok(()) } @@ -575,14 +556,14 @@ impl AsyncClient { } args.push(id.to_string()); args.push(sig.to_string()); - let _ = self.launch(self.command(&args)?, true, false).await?; + 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, false).await?; + let res = self.launch(self.command(&args)?, true).await?; let output = res.output.trim(); // Ugly hack to work around golang Ok(if output == "null" { @@ -595,7 +576,7 @@ impl AsyncClient { /// 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, false).await?; + let _ = self.launch(self.command(&args)?, true).await?; Ok(()) } @@ -606,7 +587,7 @@ impl AsyncClient { "--format-json".to_string(), id.to_string(), ]; - let res = self.launch(self.command(&args)?, true, false).await?; + let res = self.launch(self.command(&args)?, true).await?; let output = res.output.trim(); // Ugly hack to work around golang Ok(if output == "null" { @@ -623,7 +604,7 @@ impl AsyncClient { /// 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, false).await?; + let _ = self.launch(self.command(&args)?, true).await?; Ok(()) } @@ -638,28 +619,28 @@ impl AsyncClient { } args.push(utils::abs_string(bundle)?); args.push(id.to_string()); - let _ = self.launch(self.command(&args)?, true, false).await?; + 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, false).await?; + 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, false).await?; + 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, false).await?; + 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 { @@ -681,7 +662,7 @@ impl AsyncClient { filename, id.to_string(), ]; - let _ = self.launch(self.command(&args)?, true, false).await?; + let _ = self.launch(self.command(&args)?, true).await?; Ok(()) } } diff --git a/crates/runc/src/monitor.rs b/crates/runc/src/monitor.rs index 4bacb6d..4379e94 100644 --- a/crates/runc/src/monitor.rs +++ b/crates/runc/src/monitor.rs @@ -33,7 +33,6 @@ pub trait ProcessMonitor { &self, mut cmd: tokio::process::Command, tx: Sender, - forget: bool, ) -> std::io::Result { let chi = cmd.spawn()?; let pid = chi @@ -46,12 +45,7 @@ pub trait ProcessMonitor { pid, status: out.status.code().unwrap(), }) { - Ok(_) => { - if forget { - std::mem::forget(cmd); - } - Ok(out) - } + Ok(_) => Ok(out), Err(e) => { error!("command {:?} exited but receiver dropped.", cmd); error!("couldn't send messages: {:?}", e); From 522e26fab8676e5c5fc45f05103f3895c0b817e4 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 6 Feb 2022 18:20:30 +0900 Subject: [PATCH 08/10] crates/runc/src/lib.rs: fix compilation on non-Linux Signed-off-by: Akihiro Suda --- crates/runc/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/runc/src/lib.rs b/crates/runc/src/lib.rs index 403ea58..c61aa42 100644 --- a/crates/runc/src/lib.rs +++ b/crates/runc/src/lib.rs @@ -220,7 +220,7 @@ impl Client { } #[cfg(not(target_os = "linux"))] - pub fn command(&self, args: &[String]) -> Result<()> { + pub fn command(&self, _args: &[String]) -> Result { Err(Error::Unimplemented("command".to_string())) } @@ -668,6 +668,7 @@ impl AsyncClient { } #[cfg(test)] +#[cfg(target_os = "linux")] mod tests { use super::*; From 6e17be8d75f72f6f83c4430763a26b1af5afe0f5 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 6 Feb 2022 18:33:18 +0900 Subject: [PATCH 09/10] crates/shim-protos: regenerate with rust-protobuf 2.27.1 Signed-off-by: Akihiro Suda --- crates/shim-protos/src/cgroups/metrics.rs | 4 ++-- crates/shim-protos/src/events/container.rs | 4 ++-- crates/shim-protos/src/events/content.rs | 4 ++-- crates/shim-protos/src/events/image.rs | 4 ++-- crates/shim-protos/src/events/mount.rs | 4 ++-- crates/shim-protos/src/events/namespace.rs | 4 ++-- crates/shim-protos/src/events/snapshot.rs | 4 ++-- crates/shim-protos/src/events/task.rs | 4 ++-- crates/shim-protos/src/shim/empty.rs | 4 ++-- crates/shim-protos/src/shim/events.rs | 4 ++-- crates/shim-protos/src/shim/mount.rs | 4 ++-- crates/shim-protos/src/shim/shim.rs | 4 ++-- crates/shim-protos/src/shim/task.rs | 4 ++-- 13 files changed, 26 insertions(+), 26 deletions(-) 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 { From 18b5b99b847043cd7d404b06fc29a8afc8982147 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 6 Feb 2022 18:51:41 +0900 Subject: [PATCH 10/10] CI: set $XDG_RUNTIME_DIR Signed-off-by: Akihiro Suda --- .github/workflows/ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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