Merge pull request #22 from Yuna-Tomi/runc-client

runc client
This commit is contained in:
Maksym Pavlenko 2022-02-11 09:46:09 -08:00 committed by GitHub
commit 73d1dfa16d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 2559 additions and 32 deletions

View File

@ -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

View File

@ -4,5 +4,6 @@ members = [
"crates/logging",
"crates/shim-protos",
"crates/shim",
"crates/snapshots"
"crates/snapshots",
"crates/runc"
]

View File

@ -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);

29
crates/runc/Cargo.toml Normal file
View File

@ -0,0 +1,29 @@
[package]
name = "runc"
version = "0.1.0"
authors = ["Yuna Tomida <ytomida.mmm@gmail.com>", "The containerd Authors"]
edition = "2018"
license = "Apache-2.0"
repository = "https://github.com/containerd/rust-extensions"
keywords = ["containerd", "containers", "runc"]
description = "A crate for consuming the runc binary in your Rust applications"
homepage = "https://containerd.io"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-trait = "0.1.52"
futures = "0.3.19"
libc = "0.2.112"
log = "0.4.14"
nix = "0.23.1"
oci-spec = "0.5.4"
path-absolutize = "3.0.11"
rand = "0.8.4"
serde = { version = "1.0.133", features = ["derive"] }
serde_json = "1.0.74"
tempfile = "3.3.0"
thiserror = "1.0.30"
time = { version = "0.3.7", features = ["serde", "std"] }
tokio = { version = "1.15.0", features = ["full"] }
uuid = { version = "0.8.2", features = ["v4"] }

43
crates/runc/README.md Normal file
View File

@ -0,0 +1,43 @@
# Rust binding for runc client
A crate for consuming the runc binary in your Rust applications, similar to [go-runc](https://github.com/containerd/go-runc) for Go.
This crate is based on archived [rust-runc](https://github.com/pwFoo/rust-runc).
## Usage
Both sync/async version is available.
You can build runc client with `RuncConfig` in method chaining style.
Call `build()` or `build_async()` to get client.
Note that async client depends on [tokio](https://github.com/tokio-rs/tokio), then please use it on tokio runtime.
```rust
use runc;
#[tokio::main]
async fn main() {
let config = runc::Config::new()
.root("./new_root")
.debug(false)
.log("/path/to/logfile.json")
.log_format(runc::LogFormat::Json)
.rootless(true);
let client = config.build_async().unwrap();
let opts = runc::options::CreateOpts::new()
.pid_file("/path/to/pid/file")
.no_pivot(true);
client.create("container-id", "path/to/bundle", Some(&opts)).unwrap();
}
```
## Limitations
- Supported commands are only:
- create
- start
- state
- kill
- delete
- Exec is **not** available in `RuncAsyncClient` now.
- Console utilites are **not** available
- see [Go version](https://github.com/containerd/go-runc/blob/main/console.go)

View File

@ -0,0 +1,87 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Forked from https://github.com/pwFoo/rust-runc/blob/313e6ae5a79b54455b0a242a795c69adf035141a/src/lib.rs
/*
* Copyright 2020 fsyncd, Berlin, Germany.
* Additional material, copyright of the containerd authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use time::serde::timestamp;
use time::OffsetDateTime;
/// Information for runc container
#[derive(Debug, Serialize, Deserialize)]
pub struct Container {
pub id: String,
pub pid: usize,
pub status: String,
pub bundle: String,
pub rootfs: String,
#[serde(with = "timestamp")]
pub created: OffsetDateTime,
pub annotations: HashMap<String, String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serde_test() {
let j = r#"
{
"id": "fake",
"pid": 1000,
"status": "RUNNING",
"bundle": "/path/to/bundle",
"rootfs": "/path/to/rootfs",
"created": 1431684000,
"annotations": {
"foo": "bar"
}
}"#;
let c: Container = serde_json::from_str(j).unwrap();
assert_eq!(c.id, "fake");
assert_eq!(c.pid, 1000);
assert_eq!(c.status, "RUNNING");
assert_eq!(c.bundle, "/path/to/bundle");
assert_eq!(c.rootfs, "/path/to/rootfs");
assert_eq!(
c.created,
OffsetDateTime::from_unix_timestamp(1431684000).unwrap()
);
assert_eq!(c.annotations.get("foo"), Some(&"bar".to_string()));
assert_eq!(c.annotations.get("bar"), None);
}
}

119
crates/runc/src/error.rs Normal file
View File

@ -0,0 +1,119 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Forked from https://github.com/pwFoo/rust-runc/blob/313e6ae5a79b54455b0a242a795c69adf035141a/src/lib.rs
/*
* Copyright 2020 fsyncd, Berlin, Germany.
* Additional material, copyright of the containerd authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use std::env;
use std::io;
use std::process::ExitStatus;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Unable to extract test files: {0}")]
BundleExtractFailed(io::Error),
#[error("Invalid path: {0}")]
InvalidPath(io::Error),
#[error(transparent)]
JsonDeserializationFailed(#[from] serde_json::error::Error),
#[error("Missing container statistics")]
MissingContainerStats,
#[error(transparent)]
ProcessSpawnFailed(io::Error),
#[error("Error occured in runc: {0}")]
InvalidCommand(io::Error),
#[error("Runc command failed: status={status}, stdout=\"{stdout}\", stderr=\"{stderr}\"")]
CommandFailed {
status: ExitStatus,
stdout: String,
stderr: String,
},
#[error("Runc IO unavailable: {0}")]
UnavailableIO(io::Error),
#[error("Runc command timed out: {0}")]
CommandTimeout(tokio::time::error::Elapsed),
#[error("Unable to parse runc version")]
InvalidVersion,
#[error("Unable to locate the runc")]
NotFound,
#[error("Error occurs with fs: {0}")]
FileSystemError(io::Error),
#[error("Failed to spec file: {0}")]
SpecFileCreationFailed(io::Error),
#[error(transparent)]
SpecFileCleanupFailed(io::Error),
#[error("Failed to find valid path for spec file")]
SpecFileNotFound,
#[error("Top command is missing a pid header")]
TopMissingPidHeader,
#[error("Top command returned an empty response")]
TopShortResponseError,
#[error("Unix socket connection error: {0}")]
UnixSocketConnectionFailed(io::Error),
#[error("Unable to bind to unix socket: {0}")]
UnixSocketBindFailed(io::Error),
#[error("Unix socket failed to receive pty")]
UnixSocketReceiveMessageFailed,
#[error("Unix socket unexpectedly closed")]
UnixSocketClosed,
#[error("Failed to handle environment variable: {0}")]
EnvError(env::VarError),
#[error("Sorry, this part of api is not implemented: {0}")]
Unimplemented(String),
#[error("Error occured in runc client: {0}")]
Other(Box<dyn std::error::Error + Send>),
}

181
crates/runc/src/events.rs Normal file
View File

@ -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<Stats>,
}
#[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<u64>,
pub max: Option<u64>,
#[serde(rename = "failcnt")]
pub fail_count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlkIOEntry {
pub major: Option<u64>,
pub minor: Option<u64>,
pub op: Option<String>,
pub value: Option<u64>,
}
#[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<Vec<BlkIOEntry>>,
/// Number of io requests issued to the disk
#[serde(rename = "ioServicedRecursive")]
pub io_serviced_recursive: Option<Vec<BlkIOEntry>>,
/// Number of queued disk io requests
#[serde(rename = "ioQueueRecursive")]
pub io_queued_recursive: Option<Vec<BlkIOEntry>>,
/// Amount of time io requests took to service
#[serde(rename = "ioServiceTimeRecursive")]
pub io_service_time_recursive: Option<Vec<BlkIOEntry>>,
/// Amount of time io requests spent waiting in the queue
#[serde(rename = "ioWaitTimeRecursive")]
pub io_wait_time_recursive: Option<Vec<BlkIOEntry>>,
/// Number of merged io requests
#[serde(rename = "ioMergedRecursive")]
pub io_merged_recursive: Option<Vec<BlkIOEntry>>,
/// Disk time allocated the device
#[serde(rename = "ioTimeRecursive")]
pub io_time_recursive: Option<Vec<BlkIOEntry>>,
/// Number of sectors transferred to and from the io device
#[serde(rename = "sectorsRecursive")]
pub sectors_recursive: Option<Vec<BlkIOEntry>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Pids {
/// Number of pids in the cgroup
pub current: Option<u64>,
/// Active pids hard limit
pub limit: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Throttling {
/// Number of periods with throttling active
pub periods: Option<u64>,
#[serde(rename = "throttledPeriods")]
/// Number of periods when the container hit its throttling limit
pub throtted_periods: Option<u64>,
/// Aggregate time the container was throttled for in nanoseconds
#[serde(rename = "throttledTime")]
pub throtted_time: Option<u64>,
}
/// Each members represents time in nanoseconds
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CpuUsage {
/// Total CPU time consumed
pub total: Option<u64>,
/// Total CPU time consumed per core
pub per_cpu: Option<Vec<u64>>,
/// 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<u64>,
pub throttling: Option<Throttling>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryEntry {
/// Memory limit in bytes
pub limit: u64,
/// Usage in bytes
pub usage: Option<u64>,
/// Maximum usage in bytes
pub max: Option<u64>,
/// 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<u64>,
/// Overall memory usage, excluding swap
pub usage: Option<MemoryEntry>,
/// Overall memory usage, including swap
pub swap: Option<MemoryEntry>,
/// Kernel usage of memory
pub kernel: Option<MemoryEntry>,
/// Kernel TCP of memory
#[serde(rename = "kernelTCP")]
pub kernel_tcp: Option<MemoryEntry>,
/// Raw stats of memory
pub raw: Option<HashMap<String, u64>>,
}

305
crates/runc/src/io.rs Normal file
View File

@ -0,0 +1,305 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
use std::fmt::{self, Debug, Formatter};
use std::fs::File;
use std::os::unix::io::FromRawFd;
use std::os::unix::prelude::AsRawFd;
use std::process::Command;
use std::sync::Mutex;
use nix::fcntl::OFlag;
use nix::sys::stat::Mode;
use nix::unistd::{Gid, Uid};
pub trait Io: Sync + Send {
/// Return write side of stdin
fn stdin(&self) -> Option<File> {
None
}
/// Return read side of stdout
fn stdout(&self) -> Option<File> {
None
}
/// Return read side of stderr
fn stderr(&self) -> Option<File> {
None
}
/// Set IO for passed command.
/// Read side of stdin, write side of stdout and write side of stderr should be provided to command.
fn set(&self, _cmd: &mut Command) -> std::io::Result<()>;
// tokio version of set()
fn set_tk(&self, _cmd: &mut tokio::process::Command) -> std::io::Result<()>;
fn close_after_start(&self);
}
impl Debug for dyn Io {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "Io",)
}
}
#[derive(Debug, Clone)]
pub struct IOOption {
pub open_stdin: bool,
pub open_stdout: bool,
pub open_stderr: bool,
}
impl Default for IOOption {
fn default() -> Self {
Self {
open_stdin: true,
open_stdout: true,
open_stderr: true,
}
}
}
/// This struct represents pipe that can be used to transfer
/// stdio inputs and outputs
/// when one side of closed, this struct represent it with [`None`]
#[derive(Debug)]
pub struct Pipe {
// Might be ugly hack: using mutex in order to take rd/wr under immutable [`Pipe`]
rd: Mutex<Option<File>>,
wr: Mutex<Option<File>>,
}
impl Pipe {
pub fn new() -> std::io::Result<Self> {
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<File> {
let mut m = self.rd.lock().unwrap();
m.take()
}
pub fn take_write(&self) -> Option<File> {
let mut m = self.wr.lock().unwrap();
m.take()
}
pub fn close_read(&self) {
let mut m = self.rd.lock().unwrap();
let _ = m.take();
}
pub fn close_write(&self) {
let mut m = self.wr.lock().unwrap();
let _ = m.take();
}
}
#[derive(Debug)]
pub struct PipedIo {
stdin: Option<Pipe>,
stdout: Option<Pipe>,
stderr: Option<Pipe>,
}
impl PipedIo {
pub fn new(uid: u32, gid: u32, opts: IOOption) -> std::io::Result<Self> {
let uid = Some(Uid::from_raw(uid));
let gid = Some(Gid::from_raw(gid));
let stdin = if opts.open_stdin {
let pipe = Pipe::new()?;
{
let m = pipe.rd.lock().unwrap();
if let Some(f) = m.as_ref() {
nix::unistd::fchown(f.as_raw_fd(), uid, gid)?;
}
}
Some(pipe)
} else {
None
};
let stdout = if opts.open_stdout {
let pipe = Pipe::new()?;
{
let m = pipe.wr.lock().unwrap();
if let Some(f) = m.as_ref() {
nix::unistd::fchown(f.as_raw_fd(), uid, gid)?;
}
}
Some(pipe)
} else {
None
};
let stderr = if opts.open_stderr {
let pipe = Pipe::new()?;
{
let m = pipe.wr.lock().unwrap();
if let Some(f) = m.as_ref() {
nix::unistd::fchown(f.as_raw_fd(), uid, gid)?;
}
}
Some(pipe)
} else {
None
};
Ok(Self {
stdin,
stdout,
stderr,
})
}
}
impl Io for PipedIo {
fn stdin(&self) -> Option<File> {
if let Some(ref stdin) = self.stdin {
stdin.take_write()
} else {
None
}
}
fn stdout(&self) -> Option<File> {
if let Some(ref stdout) = self.stdout {
stdout.take_read()
} else {
None
}
}
fn stderr(&self) -> Option<File> {
if let Some(ref stderr) = self.stderr {
stderr.take_read()
} else {
None
}
}
/// Note that this internally use [`std::fs::File`]'s [`try_clone()`].
/// Thus, the files passed to commands will be not closed after command exit.
fn set(&self, cmd: &mut Command) -> std::io::Result<()> {
if let Some(ref p) = self.stdin {
let m = p.rd.lock().unwrap();
if let Some(stdin) = &*m {
let f = stdin.try_clone()?;
cmd.stdin(f);
}
}
if let Some(ref p) = self.stdout {
let m = p.wr.lock().unwrap();
if let Some(f) = &*m {
let f = f.try_clone()?;
cmd.stdout(f);
}
}
if let Some(ref p) = self.stderr {
let m = p.wr.lock().unwrap();
if let Some(f) = &*m {
let f = f.try_clone()?;
cmd.stderr(f);
}
}
Ok(())
}
fn set_tk(&self, cmd: &mut tokio::process::Command) -> std::io::Result<()> {
if let Some(ref p) = self.stdin {
let m = p.rd.lock().unwrap();
if let Some(stdin) = &*m {
let f = stdin.try_clone()?;
cmd.stdin(f);
}
}
if let Some(ref p) = self.stdout {
let m = p.wr.lock().unwrap();
if let Some(f) = &*m {
let f = f.try_clone()?;
cmd.stdout(f);
}
}
if let Some(ref p) = self.stderr {
let m = p.wr.lock().unwrap();
if let Some(f) = &*m {
let f = f.try_clone()?;
cmd.stderr(f);
}
}
Ok(())
}
/// closing only write side (should be stdout/err "from" runc process)
fn close_after_start(&self) {
if let Some(ref p) = self.stdout {
p.close_write();
}
if let Some(ref p) = self.stderr {
p.close_write();
}
}
}
// IO setup for /dev/null use with runc
#[derive(Debug)]
pub struct NullIo {
dev_null: Mutex<Option<File>>,
}
impl NullIo {
pub fn new() -> std::io::Result<Self> {
let fd = nix::fcntl::open("/dev/null", OFlag::O_RDONLY, Mode::empty())?;
let dev_null = unsafe { Mutex::new(Some(std::fs::File::from_raw_fd(fd))) };
Ok(Self { dev_null })
}
}
impl Io for NullIo {
fn set(&self, cmd: &mut Command) -> std::io::Result<()> {
if let Some(null) = self.dev_null.lock().unwrap().as_ref() {
cmd.stdout(null.try_clone()?);
cmd.stderr(null.try_clone()?);
}
Ok(())
}
fn set_tk(&self, cmd: &mut tokio::process::Command) -> std::io::Result<()> {
if let Some(null) = self.dev_null.lock().unwrap().as_ref() {
cmd.stdout(null.try_clone()?);
cmd.stderr(null.try_clone()?);
}
Ok(())
}
/// closing only write side (should be stdout/err "from" runc process)
fn close_after_start(&self) {
let mut m = self.dev_null.lock().unwrap();
let _ = m.take();
}
}

980
crates/runc/src/lib.rs Normal file
View File

@ -0,0 +1,980 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Forked from https://github.com/pwFoo/rust-runc/blob/313e6ae5a79b54455b0a242a795c69adf035141a/src/lib.rs
/*
* Copyright 2020 fsyncd, Berlin, Germany.
* Additional material, copyright of the containerd authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//! A crate for consuming the runc binary in your Rust applications, similar to [go-runc](https://github.com/containerd/go-runc) for Go.
use std::fmt::{self, Display};
use std::path::Path;
use std::process::{ExitStatus, Output, Stdio};
use std::time::Duration;
use oci_spec::runtime::{Linux, Process};
// suspended for difficulties
// pub mod console;
pub mod container;
pub mod error;
pub mod events;
pub mod io;
pub mod monitor;
pub mod options;
mod runc;
mod utils;
use crate::container::Container;
use crate::error::Error;
use crate::events::{Event, Stats};
use crate::monitor::{DefaultMonitor, Exit, ProcessMonitor};
use crate::options::*;
use crate::utils::{JSON, TEXT};
type Result<T> = std::result::Result<T, crate::error::Error>;
/// Response is for (pid, exit status, outputs).
#[derive(Debug, Clone)]
pub struct Response {
pub pid: u32,
pub status: ExitStatus,
pub output: String,
}
#[derive(Debug, Clone)]
pub struct Version {
pub runc_version: Option<String>,
pub spec_version: Option<String>,
pub commit: Option<String>,
}
#[derive(Debug, Clone)]
pub enum LogFormat {
Json,
Text,
}
impl Display for LogFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LogFormat::Json => write!(f, "{}", JSON),
LogFormat::Text => write!(f, "{}", TEXT),
}
}
}
/// Configuration for runc client.
///
/// This struct provide chaining interface like, for example, [`std::fs::OpenOptions`].
/// Note that you cannot access the members of Config directly.
///
/// # Example
///
/// ```ignore
/// use runc::{LogFormat, Config};
///
/// let config = Config::new()
/// .root("./new_root")
/// .debug(false)
/// .log("/path/to/logfile.json")
/// .log_format(LogFormat::Json)
/// .rootless(true);
/// let client = config.build();
/// ```
#[derive(Debug, Clone, Default)]
pub struct Config(runc::Config);
impl Config {
pub fn new() -> Self {
Self::default()
}
pub fn command<P>(&mut self, command: P) -> &mut Self
where
P: AsRef<Path>,
{
self.0.command(command);
self
}
pub fn root<P>(&mut self, root: P) -> &mut Self
where
P: AsRef<Path>,
{
self.0.root(root);
self
}
pub fn debug(&mut self, debug: bool) -> &mut Self {
self.0.debug(debug);
self
}
pub fn log<P>(&mut self, log: P) -> &mut Self
where
P: AsRef<Path>,
{
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<Client> {
Ok(Client(self.0.build()?))
}
pub fn build_async(&mut self) -> Result<AsyncClient> {
Ok(AsyncClient(self.0.build()?))
}
}
#[derive(Debug, Clone)]
pub struct Client(runc::Runc);
impl Client {
/// Create a new runc client from the supplied configuration
pub fn from_config(mut config: Config) -> Result<Self> {
config.build()
}
#[cfg(target_os = "linux")]
pub fn command(&self, args: &[String]) -> Result<std::process::Command> {
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<std::process::Command> {
Err(Error::Unimplemented("command".to_string()))
}
pub fn checkpoint(&self) -> Result<()> {
Err(Error::Unimplemented("checkpoint".to_string()))
}
fn launch(&self, mut cmd: std::process::Command, combined_output: bool) -> Result<Response> {
let child = cmd.spawn().map_err(Error::ProcessSpawnFailed)?;
let pid = child.id();
let result = child.wait_with_output().map_err(Error::InvalidCommand)?;
let status = result.status;
let stdout = String::from_utf8(result.stdout).unwrap();
let stderr = String::from_utf8(result.stderr).unwrap();
if status.success() {
if combined_output {
Ok(Response {
pid,
status,
output: stdout + stderr.as_str(),
})
} else {
Ok(Response {
pid,
status,
output: stdout,
})
}
} else {
Err(Error::CommandFailed {
status,
stdout,
stderr,
})
}
}
/// Create a new container
pub fn create<P>(&self, id: &str, bundle: P, opts: Option<&CreateOpts>) -> Result<Response>
where
P: AsRef<Path>,
{
let mut args = vec![
"create".to_string(),
"--bundle".to_string(),
utils::abs_string(bundle)?,
];
if let Some(opts) = opts {
args.append(&mut opts.args()?);
}
args.push(id.to_string());
let mut cmd = self.command(&args)?;
match opts {
Some(CreateOpts { io: Some(_io), .. }) => {
_io.set(&mut cmd).map_err(Error::UnavailableIO)?;
let res = self.launch(cmd, true)?;
_io.close_after_start();
Ok(res)
}
_ => self.launch(cmd, true),
}
}
/// Delete a container
pub fn delete(&self, id: &str, opts: Option<&DeleteOpts>) -> Result<()> {
let mut args = vec!["delete".to_string()];
if let Some(opts) = opts {
args.append(&mut opts.args());
}
args.push(id.to_string());
self.launch(self.command(&args)?, true)?;
Ok(())
}
/// Execute an additional process inside the container
pub fn exec(&self, id: &str, spec: &Process, opts: Option<&ExecOpts>) -> Result<()> {
let filename = utils::temp_filename_in_runtime_dir()?;
let spec_json = serde_json::to_string(spec).map_err(Error::JsonDeserializationFailed)?;
std::fs::write(&filename, spec_json).map_err(Error::SpecFileCreationFailed)?;
let mut args = vec!["exec".to_string(), "process".to_string(), filename];
if let Some(opts) = opts {
args.append(&mut opts.args()?);
}
args.push(id.to_string());
let mut cmd = self.command(&args)?;
if let Some(ExecOpts { io: Some(_io), .. }) = opts {
_io.set(&mut cmd).map_err(Error::UnavailableIO)?;
}
let _ = self.launch(cmd, true)?;
Ok(())
}
/// Send the specified signal to processes inside the container
pub fn kill(&self, id: &str, sig: u32, opts: Option<&KillOpts>) -> Result<()> {
let mut args = vec!["kill".to_string()];
if let Some(opts) = opts {
args.append(&mut opts.args());
}
args.push(id.to_string());
args.push(sig.to_string());
let _ = self.launch(self.command(&args)?, true)?;
Ok(())
}
/// List all containers associated with this runc instance
pub fn list(&self) -> Result<Vec<Container>> {
let args = ["list".to_string(), "--format-json".to_string()];
let res = self.launch(self.command(&args)?, true)?;
let output = res.output.trim();
// Ugly hack to work around golang
Ok(if output == "null" {
Vec::new()
} else {
serde_json::from_str(output).map_err(Error::JsonDeserializationFailed)?
})
}
/// Pause a container
pub fn pause(&self, id: &str) -> Result<()> {
let args = ["pause".to_string(), id.to_string()];
let _ = self.launch(self.command(&args)?, true)?;
Ok(())
}
pub fn restore(&self) -> Result<()> {
Err(Error::Unimplemented("restore".to_string()))
}
/// Resume a container
pub fn resume(&self, id: &str) -> Result<()> {
let args = ["pause".to_string(), id.to_string()];
let _ = self.launch(self.command(&args)?, true)?;
Ok(())
}
/// Run the create, start, delete lifecycle of the container and return its exit status
pub fn run<P>(&self, id: &str, bundle: P, opts: Option<&CreateOpts>) -> Result<Response>
where
P: AsRef<Path>,
{
let mut args = vec!["run".to_string(), "--bundle".to_string()];
if let Some(opts) = opts {
args.append(&mut opts.args()?);
}
args.push(utils::abs_string(bundle)?);
args.push(id.to_string());
let mut cmd = self.command(&args)?;
if let Some(CreateOpts { io: Some(_io), .. }) = opts {
_io.set(&mut cmd).map_err(Error::UnavailableIO)?;
};
self.launch(self.command(&args)?, true)
}
/// Start an already created container
pub fn start(&self, id: &str) -> Result<Response> {
let args = ["start".to_string(), id.to_string()];
self.launch(self.command(&args)?, true)
}
/// Return the state of a container
pub fn state(&self, id: &str) -> Result<Container> {
let args = ["state".to_string(), id.to_string()];
let res = self.launch(self.command(&args)?, true)?;
serde_json::from_str(&res.output).map_err(Error::JsonDeserializationFailed)
}
/// Update a container with the provided resource spec
pub fn update(&self, id: &str, resources: &Linux) -> Result<()> {
let filename = utils::temp_filename_in_runtime_dir()?;
let spec_json =
serde_json::to_string(resources).map_err(Error::JsonDeserializationFailed)?;
std::fs::write(&filename, spec_json).map_err(Error::SpecFileCreationFailed)?;
let args = [
"update".to_string(),
"--resources".to_string(),
filename,
id.to_string(),
];
self.launch(self.command(&args)?, true)?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct AsyncClient(runc::Runc);
// As monitor instance never have to be mutable (it has only &self methods), declare it as const.
const MONITOR: DefaultMonitor = DefaultMonitor::new();
/// Async client for runc
/// Note that you MUST use this client on tokio runtime, as this client internally use [`tokio::process::Command`]
/// and some other utilities.
impl AsyncClient {
/// Create a new runc client from the supplied configuration
pub fn from_config(mut config: Config) -> Result<Self> {
config.build_async()
}
pub fn command(&self, args: &[String]) -> Result<tokio::process::Command> {
let args = [&self.0.args()?, args].concat();
let mut cmd = tokio::process::Command::new(&self.0.command);
cmd.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
cmd.args(&args).env_remove("NOTIFY_SOCKET"); // NOTIFY_SOCKET introduces a special behavior in runc but should only be set if invoked from systemd
Ok(cmd)
}
pub async fn launch(
&self,
cmd: tokio::process::Command,
combined_output: bool,
) -> Result<Response> {
let (tx, rx) = tokio::sync::oneshot::channel::<Exit>();
let start = MONITOR.start(cmd, tx);
let wait = MONITOR.wait(rx);
let (
Output {
status,
stdout,
stderr,
},
Exit { pid, .. },
) = tokio::try_join!(start, wait).map_err(Error::InvalidCommand)?;
// ugly hack to work around
let stdout = String::from_utf8(stdout)
.expect("returned non-utf8 characters from container process.");
let stderr = String::from_utf8(stderr)
.expect("returned non-utf8 characters from container process.");
if status.success() {
if combined_output {
Ok(Response {
pid,
status,
output: stdout + stderr.as_str(),
})
} else {
Ok(Response {
pid,
status,
output: stdout,
})
}
} else {
Err(Error::CommandFailed {
status,
stdout,
stderr,
})
}
}
pub async fn checkpoint(&self) -> Result<()> {
Err(Error::Unimplemented("checkpoint".to_string()))
}
/// Create a new container
pub async fn create<P>(&self, id: &str, bundle: P, opts: Option<&CreateOpts>) -> Result<()>
where
P: AsRef<Path>,
{
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::<Exit>();
let start = MONITOR.start(cmd, tx);
let wait = MONITOR.wait(rx);
let (
Output {
status,
stdout,
stderr,
},
_,
) = tokio::try_join!(start, wait).map_err(Error::InvalidCommand)?;
_io.close_after_start();
let stdout = String::from_utf8(stdout).unwrap();
let stderr = String::from_utf8(stderr).unwrap();
if !status.success() {
return Err(Error::CommandFailed {
status,
stdout,
stderr,
});
}
}
_ => {
let _ = self.launch(cmd, true).await?;
}
}
Ok(())
}
/// Delete a container
pub async fn delete(&self, id: &str, opts: Option<&DeleteOpts>) -> Result<()> {
let mut args = vec!["delete".to_string()];
if let Some(opts) = opts {
args.append(&mut opts.args());
}
args.push(id.to_string());
let _ = self.launch(self.command(&args)?, true).await?;
Ok(())
}
/// Return an event stream of container notifications
pub async fn events(&self, _id: &str, _interval: &Duration) -> Result<()> {
Err(Error::Unimplemented("events".to_string()))
}
/// Execute an additional process inside the container
pub async fn exec(&self, _id: &str, _spec: &Process, _opts: Option<&ExecOpts>) -> Result<()> {
Err(Error::Unimplemented("exec".to_string()))
}
/// Send the specified signal to processes inside the container
pub async fn kill(&self, id: &str, sig: u32, opts: Option<&KillOpts>) -> Result<()> {
let mut args = vec!["kill".to_string()];
if let Some(opts) = opts {
args.append(&mut opts.args());
}
args.push(id.to_string());
args.push(sig.to_string());
let _ = self.launch(self.command(&args)?, true).await?;
Ok(())
}
/// List all containers associated with this runc instance
pub async fn list(&self) -> Result<Vec<Container>> {
let args = ["list".to_string(), "--format-json".to_string()];
let res = self.launch(self.command(&args)?, true).await?;
let output = res.output.trim();
// Ugly hack to work around golang
Ok(if output == "null" {
Vec::new()
} else {
serde_json::from_str(output).map_err(Error::JsonDeserializationFailed)?
})
}
/// Pause a container
pub async fn pause(&self, id: &str) -> Result<()> {
let args = ["pause".to_string(), id.to_string()];
let _ = self.launch(self.command(&args)?, true).await?;
Ok(())
}
/// List all the processes inside the container, returning their pids
pub async fn ps(&self, id: &str) -> Result<Vec<usize>> {
let args = [
"ps".to_string(),
"--format-json".to_string(),
id.to_string(),
];
let res = self.launch(self.command(&args)?, true).await?;
let output = res.output.trim();
// Ugly hack to work around golang
Ok(if output == "null" {
Vec::new()
} else {
serde_json::from_str(output).map_err(Error::JsonDeserializationFailed)?
})
}
pub async fn restore(&self) -> Result<()> {
Err(Error::Unimplemented("restore".to_string()))
}
/// Resume a container
pub async fn resume(&self, id: &str) -> Result<()> {
let args = ["pause".to_string(), id.to_string()];
let _ = self.launch(self.command(&args)?, true).await?;
Ok(())
}
/// Run the create, start, delete lifecycle of the container and return its exit status
pub async fn run<P>(&self, id: &str, bundle: P, opts: Option<&CreateOpts>) -> Result<()>
where
P: AsRef<Path>,
{
let mut args = vec!["run".to_string(), "--bundle".to_string()];
if let Some(opts) = opts {
args.append(&mut opts.args()?);
}
args.push(utils::abs_string(bundle)?);
args.push(id.to_string());
let _ = self.launch(self.command(&args)?, true).await?;
Ok(())
}
/// Start an already created container
pub async fn start(&self, id: &str) -> Result<()> {
let args = vec!["start".to_string(), id.to_string()];
let _ = self.launch(self.command(&args)?, true).await?;
Ok(())
}
/// Return the state of a container
pub async fn state(&self, id: &str) -> Result<Vec<usize>> {
let args = vec!["state".to_string(), id.to_string()];
let res = self.launch(self.command(&args)?, true).await?;
serde_json::from_str(&res.output).map_err(Error::JsonDeserializationFailed)
}
/// Return the latest statistics for a container
pub async fn stats(&self, id: &str) -> Result<Stats> {
let args = vec!["events".to_string(), "--stats".to_string(), id.to_string()];
let res = self.launch(self.command(&args)?, true).await?;
let event: Event =
serde_json::from_str(&res.output).map_err(Error::JsonDeserializationFailed)?;
if let Some(stats) = event.stats {
Ok(stats)
} else {
Err(Error::MissingContainerStats)
}
}
/// Update a container with the provided resource spec
pub async fn update(&self, id: &str, resources: &Linux) -> Result<()> {
let filename = utils::temp_filename_in_runtime_dir()?;
let spec_json =
serde_json::to_string(resources).map_err(Error::JsonDeserializationFailed)?;
std::fs::write(&filename, spec_json).map_err(Error::SpecFileCreationFailed)?;
let args = vec![
"update".to_string(),
"--resources".to_string(),
filename,
id.to_string(),
];
let _ = self.launch(self.command(&args)?, true).await?;
Ok(())
}
}
#[cfg(test)]
#[cfg(target_os = "linux")]
mod tests {
use super::*;
// following style of go-runc, use only true/false to test
const CMD_TRUE: &str = "/bin/true";
const CMD_FALSE: &str = "/bin/false";
fn ok_client() -> Client {
Config::new()
.command(CMD_TRUE)
.build()
.expect("unable to create runc instance")
}
fn fail_client() -> Client {
Config::new()
.command(CMD_FALSE)
.build()
.expect("unable to create runc instance")
}
fn ok_async_client() -> AsyncClient {
Config::new()
.command(CMD_TRUE)
.build_async()
.expect("unable to create runc instance")
}
fn fail_async_client() -> AsyncClient {
Config::new()
.command(CMD_FALSE)
.build_async()
.expect("unable to create runc instance")
}
fn dummy_process() -> Process {
serde_json::from_str(
"
{
\"user\": {
\"uid\": 1000,
\"gid\": 1000
},
\"cwd\": \"/path/to/dir\"
}",
)
.unwrap()
}
#[test]
fn test_create() {
let opts = CreateOpts::new();
let ok_runc = ok_client();
ok_runc
.create("fake-id", "fake-bundle", Some(&opts))
.expect("true failed.");
eprintln!("ok_runc succeeded.");
let fail_runc = fail_client();
match fail_runc.create("fake-id", "fake-bundle", Some(&opts)) {
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
}
#[test]
fn test_run() {
let opts = CreateOpts::new();
let ok_runc = ok_client();
ok_runc
.run("fake-id", "fake-bundle", Some(&opts))
.expect("true failed.");
eprintln!("ok_runc succeeded.");
let fail_runc = fail_client();
match fail_runc.run("fake-id", "fake-bundle", Some(&opts)) {
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
}
#[test]
fn test_exec() {
let opts = ExecOpts::new();
let ok_runc = ok_client();
let proc = dummy_process();
ok_runc
.exec("fake-id", &proc, Some(&opts))
.expect("true failed.");
eprintln!("ok_runc succeeded.");
let fail_runc = fail_client();
match fail_runc.exec("fake-id", &proc, Some(&opts)) {
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
}
#[test]
fn test_delete() {
let opts = DeleteOpts::new();
let ok_runc = ok_client();
ok_runc
.delete("fake-id", Some(&opts))
.expect("true failed.");
eprintln!("ok_runc succeeded.");
let fail_runc = fail_client();
match fail_runc.delete("fake-id", Some(&opts)) {
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
}
#[tokio::test]
async fn test_async_create() {
let opts = CreateOpts::new();
let ok_runc = ok_async_client();
let ok_task = tokio::spawn(async move {
ok_runc
.create("fake-id", "fake-bundle", Some(&opts))
.await
.expect("true failed.");
eprintln!("ok_runc succeeded.");
});
let opts = CreateOpts::new();
let fail_runc = fail_async_client();
let fail_task = tokio::spawn(async move {
match fail_runc
.create("fake-id", "fake-bundle", Some(&opts))
.await
{
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
});
ok_task.await.expect("ok_task failed.");
fail_task.await.expect("fail_task unexpectedly succeeded.");
}
#[tokio::test]
async fn test_async_start() {
let ok_runc = ok_async_client();
let ok_task = tokio::spawn(async move {
ok_runc.start("fake-id").await.expect("true failed.");
eprintln!("ok_runc succeeded.");
});
let fail_runc = fail_async_client();
let fail_task = tokio::spawn(async move {
match fail_runc.start("fake-id").await {
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
});
ok_task.await.expect("ok_task failed.");
fail_task.await.expect("fail_task unexpectedly succeeded.");
}
#[tokio::test]
async fn test_async_run() {
let opts = CreateOpts::new();
let ok_runc = Config::new()
.command(CMD_TRUE)
.build_async()
.expect("unable to create runc instance");
tokio::spawn(async move {
ok_runc
.create("fake-id", "fake-bundle", Some(&opts))
.await
.expect("true failed.");
eprintln!("ok_runc succeeded.");
});
let opts = CreateOpts::new();
let fail_runc = Config::new()
.command(CMD_FALSE)
.build_async()
.expect("unable to create runc instance");
tokio::spawn(async move {
match fail_runc
.create("fake-id", "fake-bundle", Some(&opts))
.await
{
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
})
.await
.expect("tokio spawn falied.");
}
#[tokio::test]
async fn test_async_delete() {
let opts = DeleteOpts::new();
let ok_runc = Config::new()
.command(CMD_TRUE)
.build_async()
.expect("unable to create runc instance");
tokio::spawn(async move {
ok_runc
.delete("fake-id", Some(&opts))
.await
.expect("true failed.");
eprintln!("ok_runc succeeded.");
});
let opts = DeleteOpts::new();
let fail_runc = Config::new()
.command(CMD_FALSE)
.build_async()
.expect("unable to create runc instance");
tokio::spawn(async move {
match fail_runc.delete("fake-id", Some(&opts)).await {
Ok(_) => panic!("fail_runc returned exit status 0."),
Err(Error::CommandFailed {
status,
stdout,
stderr,
}) => {
if status.code().unwrap() == 1 && stdout.is_empty() && stderr.is_empty() {
eprintln!("fail_runc succeeded.");
} else {
panic!("unexpected outputs from fail_runc.")
}
}
Err(e) => panic!("unexpected error from fail_runc: {:?}", e),
}
})
.await
.expect("tokio spawn falied.");
}
}

View File

@ -0,0 +1,80 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
use std::process::Output;
use async_trait::async_trait;
use log::error;
use time::OffsetDateTime;
use tokio::sync::oneshot::{Receiver, Sender};
// ProcessMonitor for handling runc process exit
// Implementation is different from Go's, because if you return Sender in start() and want to
// use it in wait(), then start and wait cannot be executed concurrently.
// Alternatively, caller of start() and wait() have to prepare channel
#[async_trait]
pub trait ProcessMonitor {
/// Caller cand choose [`std::mem::forget`] about resource
/// associated to that command, e.g. file descriptors.
async fn start(
&self,
mut cmd: tokio::process::Command,
tx: Sender<Exit>,
) -> std::io::Result<Output> {
let chi = cmd.spawn()?;
let pid = chi
.id()
.expect("failed to take pid of the container process.");
let out = chi.wait_with_output().await?;
let ts = OffsetDateTime::now_utc();
match tx.send(Exit {
ts,
pid,
status: out.status.code().unwrap(),
}) {
Ok(_) => Ok(out),
Err(e) => {
error!("command {:?} exited but receiver dropped.", cmd);
error!("couldn't send messages: {:?}", e);
Err(std::io::ErrorKind::ConnectionRefused.into())
}
}
}
async fn wait(&self, rx: Receiver<Exit>) -> std::io::Result<Exit> {
rx.await.map_err(|_| {
error!("sender dropped.");
std::io::ErrorKind::BrokenPipe.into()
})
}
}
#[derive(Debug, Clone, Default)]
pub struct DefaultMonitor {}
impl ProcessMonitor for DefaultMonitor {}
impl DefaultMonitor {
pub const fn new() -> Self {
Self {}
}
}
#[derive(Debug)]
pub struct Exit {
pub ts: OffsetDateTime,
pub pid: u32,
pub status: i32,
}

371
crates/runc/src/options.rs Normal file
View File

@ -0,0 +1,371 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Forked from https://github.com/pwFoo/rust-runc/blob/313e6ae5a79b54455b0a242a795c69adf035141a/src/lib.rs
/*
* Copyright 2020 fsyncd, Berlin, Germany.
* Additional material, copyright of the containerd authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::error::Error;
use crate::io::Io;
use crate::utils::{self, ALL, CONSOLE_SOCKET, DETACH, FORCE, NO_NEW_KEYRING, NO_PIVOT, PID_FILE};
pub trait Args {
type Output;
fn args(&self) -> Self::Output;
}
#[derive(Debug, Clone, Default)]
pub struct CreateOpts {
pub io: Option<Arc<dyn Io>>,
/// Path to where a pid file should be created.
pub pid_file: Option<PathBuf>,
/// Path to where a console socket should be created.
pub console_socket: Option<PathBuf>,
/// 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<Vec<String>, Error>;
fn args(&self) -> Self::Output {
let mut args: Vec<String> = 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<dyn Io>) -> Self {
self.io = Some(io);
self
}
pub fn pid_file<P>(mut self, pid_file: P) -> Self
where
P: AsRef<Path>,
{
self.pid_file = Some(pid_file.as_ref().to_path_buf());
self
}
pub fn console_socket<P>(mut self, console_socket: P) -> Self
where
P: AsRef<Path>,
{
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<Arc<dyn Io>>,
/// Path to where a pid file should be created.
pub pid_file: Option<PathBuf>,
/// Path to where a console socket should be created.
pub console_socket: Option<PathBuf>,
/// Detach from the container's process (only available for run)
pub detach: bool,
}
impl Args for ExecOpts {
type Output = Result<Vec<String>, Error>;
fn args(&self) -> Self::Output {
let mut args: Vec<String> = 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<dyn Io>) -> Self {
self.io = Some(io);
self
}
pub fn pid_file<P>(mut self, pid_file: P) -> Self
where
P: AsRef<Path>,
{
self.pid_file = Some(pid_file.as_ref().to_path_buf());
self
}
pub fn console_socket<P>(mut self, console_socket: P) -> Self
where
P: AsRef<Path>,
{
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<String>;
fn args(&self) -> Self::Output {
let mut args: Vec<String> = 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<String>;
fn args(&self) -> Self::Output {
let mut args: Vec<String> = 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::<String>()
.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::<String>()
.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::<String>()
.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::<String>()
.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()],);
}
}

210
crates/runc/src/runc.rs Normal file
View File

@ -0,0 +1,210 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Forked from https://github.com/pwFoo/rust-runc/blob/313e6ae5a79b54455b0a242a795c69adf035141a/src/lib.rs
/*
* Copyright 2020 fsyncd, Berlin, Germany.
* Additional material, copyright of the containerd authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use std::path::{Path, PathBuf};
use std::time::Duration;
use crate::error::Error;
use crate::options::Args;
use crate::utils::{self, DEBUG, DEFAULT_COMMAND, LOG, LOG_FORMAT, ROOT, ROOTLESS, SYSTEMD_CGROUP};
use crate::LogFormat;
/// Inner struct for runc configuration
#[derive(Debug, Clone, Default)]
pub struct Config {
/// This field is set to overrides the name of the runc binary. If [`None`], "runc" is used.
command: Option<PathBuf>,
/// Path to root directory of container rootfs.
root: Option<PathBuf>,
/// Debug logging. If true, debug level logs are emitted.
debug: bool,
/// Path to log file.
log: Option<PathBuf>,
/// Specifyng log format. Here, json or text is available. Default is "text" and interpreted as text if [`None`].
log_format: Option<LogFormat>,
// 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<bool>,
// FIXME: implementation of extra_args is suspended due to difficulties.
// extra_args: Vec<String>,
/// Timeout settings for runc command. Default is 5 seconds.
/// This will be used only in AsyncClient.
timeout: Option<Duration>,
}
impl Config {
pub fn command<P>(&mut self, command: P)
where
P: AsRef<Path>,
{
self.command = Some(command.as_ref().to_path_buf());
}
pub fn root<P>(&mut self, root: P)
where
P: AsRef<Path>,
{
self.root = Some(root.as_ref().to_path_buf());
}
pub fn debug(&mut self, debug: bool) {
self.debug = debug;
}
pub fn log<P>(&mut self, log: P)
where
P: AsRef<Path>,
{
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<Runc, Error> {
let command = utils::binary_path(
self.command
.clone()
.unwrap_or_else(|| PathBuf::from(DEFAULT_COMMAND)),
)
.ok_or(Error::NotFound)?;
Ok(Runc {
command,
root: self.root.clone(),
debug: self.debug,
log: self.log.clone(),
log_format: self.log_format.clone().unwrap_or(LogFormat::Text),
// self.pdeath_signal: self.pdeath_signal,
systemd_cgroup: self.systemd_cgroup,
set_pgid: self.set_pgid,
// criu: self.criu,
rootless: self.rootless,
// extra_args: self.extra_args,
timeout: self.timeout.unwrap_or_else(|| Duration::from_millis(5000)),
})
}
}
/// Inner Runtime for RuncClient/RuncAsyncClient
#[derive(Debug, Clone)]
pub struct Runc {
pub command: PathBuf,
pub root: Option<PathBuf>,
pub debug: bool,
pub log: Option<PathBuf>,
pub log_format: LogFormat,
// pdeath_signal: XXX,
pub set_pgid: bool,
// criu: bool,
pub systemd_cgroup: bool,
pub rootless: Option<bool>,
// extra_args: Vec<String>,
pub timeout: Duration,
}
impl Args for Runc {
type Output = Result<Vec<String>, Error>;
fn args(&self) -> Self::Output {
let mut args: Vec<String> = 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)
}
}

116
crates/runc/src/utils.rs Normal file
View File

@ -0,0 +1,116 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#![allow(unused)]
use std::env;
use std::path::{Path, PathBuf};
use path_absolutize::*;
use tempfile::{Builder, NamedTempFile};
use uuid::Uuid;
use crate::error::Error;
// constants for flags
pub const ALL: &str = "--all";
pub const CONSOLE_SOCKET: &str = "--console-socket";
// pub const CRIU: &str = "--criu";
pub const DEBUG: &str = "--debug";
pub const DETACH: &str = "--detach";
pub const FORCE: &str = "--force";
pub const LOG: &str = "--log";
pub const LOG_FORMAT: &str = "--log-format";
pub const NO_NEW_KEYRING: &str = "--no-new-keyring";
pub const NO_PIVOT: &str = "--no-pivot";
pub const PID_FILE: &str = "--pid-file";
pub const ROOT: &str = "--root";
pub const ROOTLESS: &str = "--rootless";
pub const SYSTEMD_CGROUP: &str = "--systemd-cgroup";
// constants for log format
pub const JSON: &str = "json";
pub const TEXT: &str = "text";
// constant for command
pub const DEFAULT_COMMAND: &str = "runc";
// helper to resolve path (such as path for runc binary, pid files, etc. )
pub fn abs_path_buf<P>(path: P) -> Result<PathBuf, Error>
where
P: AsRef<Path>,
{
Ok(path
.as_ref()
.absolutize()
.map_err(Error::InvalidPath)?
.to_path_buf())
}
pub fn abs_string<P>(path: P) -> Result<String, Error>
where
P: AsRef<Path>,
{
Ok(abs_path_buf(path)?
.to_string_lossy()
.parse::<String>()
.unwrap())
}
pub fn temp_filename_in_runtime_dir() -> Result<String, Error> {
env::var_os("XDG_RUNTIME_DIR")
.map(|runtime_dir| {
format!(
"{}/runc-process-{}",
runtime_dir.to_string_lossy().parse::<String>().unwrap(),
Uuid::new_v4(),
)
})
.ok_or(Error::SpecFileNotFound)
}
pub fn make_temp_file_in_runtime_dir() -> Result<(NamedTempFile, String), Error> {
let file_name = env::var_os("XDG_RUNTIME_DIR")
.map(|runtime_dir| {
format!(
"{}/runc-process-{}",
runtime_dir.to_string_lossy().parse::<String>().unwrap(),
Uuid::new_v4(),
)
})
.ok_or(Error::SpecFileNotFound)?;
let temp_file = Builder::new()
.prefix(&file_name)
.tempfile()
.map_err(Error::SpecFileCreationFailed)?;
Ok((temp_file, file_name))
}
pub fn binary_path<P>(path: P) -> Option<PathBuf>
where
P: AsRef<Path>,
{
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
}
})
})
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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<S: AsRef<OsStr>>(args: &[S]) -> Result<Flags, Error> {
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) {

View File

@ -1,3 +1,3 @@
[toolchain]
channel = "1.52"
channel = "1.54"
components = ["rustfmt", "clippy"]