mirror of
https://github.com/kata-containers/kata-containers.git
synced 2025-04-30 20:54:26 +00:00
tools: Add runk
Add a Rust-based standard OCI container runtime based on Kata agent. You can build and install runk as follows: ```sh $ cd src/tools/runk $ make $ sudo make install $ runk --help ``` Fixes: #2784 Signed-off-by: Manabu Sugimoto <Manabu.Sugimoto@sony.com>
This commit is contained in:
parent
2c218a07b9
commit
b221a2590f
1
Makefile
1
Makefile
@ -14,6 +14,7 @@ TOOLS =
|
||||
|
||||
TOOLS += agent-ctl
|
||||
TOOLS += trace-forwarder
|
||||
TOOLS += runk
|
||||
|
||||
STANDARD_TARGETS = build check clean install test vendor
|
||||
|
||||
|
@ -132,6 +132,7 @@ The table below lists the remaining parts of the project:
|
||||
| [osbuilder](tools/osbuilder) | infrastructure | Tool to create "mini O/S" rootfs and initrd images and kernel for the hypervisor. |
|
||||
| [`agent-ctl`](src/tools/agent-ctl) | utility | Tool that provides low-level access for testing the agent. |
|
||||
| [`trace-forwarder`](src/tools/trace-forwarder) | utility | Agent tracing helper. |
|
||||
| [`runk`](src/tools/runk) | utility | Standard OCI container runtime based on the agent. |
|
||||
| [`ci`](https://github.com/kata-containers/ci) | CI | Continuous Integration configuration files and scripts. |
|
||||
| [`katacontainers.io`](https://github.com/kata-containers/www.katacontainers.io) | Source for the [`katacontainers.io`](https://www.katacontainers.io) site. |
|
||||
|
||||
|
1
src/tools/runk/.gitignore
vendored
Normal file
1
src/tools/runk/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/vendor/
|
1474
src/tools/runk/Cargo.lock
generated
Normal file
1474
src/tools/runk/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
src/tools/runk/Cargo.toml
Normal file
29
src/tools/runk/Cargo.toml
Normal file
@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "runk"
|
||||
version = "0.0.1"
|
||||
authors = ["The Kata Containers community <kata-dev@lists.katacontainers.io>"]
|
||||
description = "runk: Kata OCI container runtime based on Kata agent"
|
||||
license = "Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
libcontainer = { path = "./libcontainer" }
|
||||
rustjail = { path = "../../agent/rustjail", features = ["standard-oci-runtime"] }
|
||||
oci = { path = "../../libs/oci" }
|
||||
logging = { path = "../../libs/logging" }
|
||||
liboci-cli = "0.0.3"
|
||||
clap = { version = "3.0.6", features = ["derive", "cargo"] }
|
||||
libc = "0.2.108"
|
||||
nix = "0.23.0"
|
||||
anyhow = "1.0.52"
|
||||
slog = "2.7.0"
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
slog-async = "2.7.0"
|
||||
tokio = { version = "1.15.0", features = ["full"] }
|
||||
serde = { version = "1.0.133", features = ["derive"] }
|
||||
serde_json = "1.0.74"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"libcontainer"
|
||||
]
|
60
src/tools/runk/Makefile
Normal file
60
src/tools/runk/Makefile
Normal file
@ -0,0 +1,60 @@
|
||||
# Copyright 2021-2022 Sony Group Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
include ../../../utils.mk
|
||||
|
||||
TARGET = runk
|
||||
TARGET_PATH = target/$(TRIPLE)/$(BUILD_TYPE)/$(TARGET)
|
||||
|
||||
AGENT_TARGET = oci-kata-agent
|
||||
AGENT_TARGET_PATH = target/$(TRIPLE)/$(BUILD_TYPE)/$(AGENT_TARGET)
|
||||
AGENT_SOURCE_PATH = ../../agent
|
||||
|
||||
# BINDIR is a directory for installing executable programs
|
||||
BINDIR := /usr/local/bin
|
||||
|
||||
.DEFAULT_GOAL := default
|
||||
default: build
|
||||
|
||||
build: build-agent build-runk
|
||||
|
||||
build-agent:
|
||||
make -C $(AGENT_SOURCE_PATH) STANDARD_OCI_RUNTIME=yes
|
||||
|
||||
build-runk:
|
||||
@RUSTFLAGS="$(EXTRA_RUSTFLAGS) --deny warnings" cargo build --target $(TRIPLE) --$(BUILD_TYPE)
|
||||
|
||||
install: install-agent install-runk
|
||||
|
||||
install-agent:
|
||||
install -D $(AGENT_SOURCE_PATH)/$(AGENT_TARGET_PATH) $(BINDIR)/$(AGENT_TARGET)
|
||||
|
||||
install-runk:
|
||||
install -D $(TARGET_PATH) $(BINDIR)/$(TARGET)
|
||||
|
||||
clean:
|
||||
cargo clean
|
||||
|
||||
vendor:
|
||||
cargo vendor
|
||||
|
||||
test:
|
||||
cargo test --all --target $(TRIPLE) -- --nocapture
|
||||
|
||||
check: standard_rust_check
|
||||
|
||||
.PHONY: \
|
||||
build \
|
||||
build-agent \
|
||||
build-runk \
|
||||
install \
|
||||
install-agent \
|
||||
install-runk \
|
||||
clean \
|
||||
clippy \
|
||||
format \
|
||||
vendor \
|
||||
test \
|
||||
check \
|
282
src/tools/runk/README.md
Normal file
282
src/tools/runk/README.md
Normal file
@ -0,0 +1,282 @@
|
||||
# runk
|
||||
|
||||
## Overview
|
||||
|
||||
> **Warnings:**
|
||||
> `runk` is currently an experimental tool.
|
||||
> Only continue if you are using a non-critical system.
|
||||
|
||||
`runk` is a standard OCI container runtime written in Rust based on a modified version of
|
||||
the [Kata Container agent](https://github.com/kata-containers/kata-containers/tree/main/src/agent), `kata-agent`.
|
||||
|
||||
`runk` conforms to the [OCI Container Runtime specifications](https://github.com/opencontainers/runtime-spec).
|
||||
|
||||
Unlike the [Kata Container runtime](https://github.com/kata-containers/kata-containers/tree/main/src/agent#features),
|
||||
`kata-runtime`, `runk` spawns and runs containers on the host machine directly.
|
||||
The user can run `runk` in the same way as the existing container runtimes such as `runc`,
|
||||
the most used implementation of the OCI runtime specs.
|
||||
|
||||
## Why does `runk` exist?
|
||||
|
||||
The `kata-agent` is a process running inside a virtual machine (VM) as a supervisor for managing containers
|
||||
and processes running within those containers.
|
||||
In other words, the `kata-agent` is a kind of "low-level" container runtime inside VM because the agent
|
||||
spawns and runs containers according to the OCI runtime specs.
|
||||
However, the `kata-agent` does not have the OCI Command-Line Interface (CLI) that is defined in the
|
||||
[runtime spec](https://github.com/opencontainers/runtime-spec/blob/master/runtime.md).
|
||||
The `kata-runtime` provides the CLI part of the Kata Containers runtime component,
|
||||
but the `kata-runtime` is a container runtime for creating hardware-virtualized containers running on the host.
|
||||
|
||||
`runk` is a Rust-based standard OCI container runtime that manages normal containers,
|
||||
not hardware-virtualized containers.
|
||||
`runk` aims to become one of the alternatives to existing OCI compliant container runtimes.
|
||||
The `kata-agent` has most of the [features](https://github.com/kata-containers/kata-containers/tree/main/src/agent#features)
|
||||
needed for the container runtime and delivers high performance with a low memory footprint owing to the
|
||||
implementation by Rust language.
|
||||
Therefore, `runk` leverages the mechanism of the `kata-agent` to avoid reinventing the wheel.
|
||||
|
||||
## Performance
|
||||
|
||||
`runk` is faster than `runc` and has a lower memory footprint.
|
||||
|
||||
This table shows the average of the elapsed time and the memory footprint (maximum resident set size)
|
||||
for running sequentially 100 containers, the containers run `/bin/true` using `run` command with
|
||||
[detached mode](https://github.com/opencontainers/runc/blob/master/docs/terminals.md#detached)
|
||||
on 12 CPU cores (`3.8 GHz AMD Ryzen 9 3900X`) and 32 GiB of RAM.
|
||||
`runk` always runs containers with detached mode currently.
|
||||
|
||||
Evaluation Results:
|
||||
|
||||
| | `runk` (v0.0.1) | `runc` (v1.0.3) | `crun` (v1.4.2) |
|
||||
|-----------------------|---------------|---------------|---------------|
|
||||
| time [ms] | 39.83 | 50.39 | 38.41 |
|
||||
| memory footprint [MB] | 4.013 | 10.78 | 1.738 |
|
||||
|
||||
## Status of `runk`
|
||||
|
||||
We drafted the initial code here, and any contributions to `runk` and [`kata-agent`](https://github.com/kata-containers/kata-containers/tree/main/src/agent)
|
||||
are welcome.
|
||||
|
||||
Regarding features compared to `runc`, see the `Status of runk` section in the [issue](https://github.com/kata-containers/kata-containers/issues/2784).
|
||||
|
||||
## Building
|
||||
|
||||
`runk` uses the modified the `kata-agent` binary, `oci-kata-agent`, which is an agent to be called from `runk`.
|
||||
Therefore, you also need to build the `oci-kata-agent` to run `runk`.
|
||||
|
||||
You can build both `runk` and `oci-kata-agent` as follows.
|
||||
|
||||
```bash
|
||||
$ cd runk
|
||||
$ make
|
||||
```
|
||||
|
||||
To install `runk` and `oci-kata-agent` into default directory for install executable program (`/usr/local/bin`):
|
||||
|
||||
```bash
|
||||
$ sudo make install
|
||||
```
|
||||
|
||||
## Using `runk` directly
|
||||
|
||||
Please note that `runk` is a low level tool not developed with an end user in mind.
|
||||
It is mostly employed by other higher-level container software like `containerd`.
|
||||
|
||||
If you still want to use `runk` directly, here's how.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
It is necessary to create an OCI bundle to use the tool. The simplest method is:
|
||||
|
||||
``` bash
|
||||
$ bundle_dir="bundle"
|
||||
$ rootfs_dir="$bundle_dir/rootfs"
|
||||
$ image="busybox"
|
||||
$ mkdir -p "$rootfs_dir" && (cd "$bundle_dir" && runk spec)
|
||||
$ sudo docker export $(sudo docker create "$image") | tar -C "$rootfs_dir" -xvf -
|
||||
```
|
||||
|
||||
> **Note:**
|
||||
> If you use the unmodified `runk spec` template, this should give a `sh` session inside the container.
|
||||
> However, if you use `runk` directly and run a container with the unmodified template,
|
||||
> `runk` cannot launch the `sh` session because `runk` does not support terminal handling yet.
|
||||
> You need to edit the process field in the `config.json` should look like this below
|
||||
> with `"terminal": false` and `"args": ["sleep", "10"]`.
|
||||
|
||||
```json
|
||||
"process": {
|
||||
"terminal": false,
|
||||
"user": {
|
||||
"uid": 0,
|
||||
"gid": 0
|
||||
},
|
||||
"args": [
|
||||
"sleep",
|
||||
"10"
|
||||
],
|
||||
"env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"TERM=xterm"
|
||||
],
|
||||
"cwd": "/",
|
||||
[...]
|
||||
}
|
||||
```
|
||||
|
||||
If you want to launch the `sh` session inside the container, you need to run `runk` from `containerd`.
|
||||
|
||||
Please refer to the [Using `runk` from containerd](#using-runk-from-containerd) section
|
||||
|
||||
### Running a container
|
||||
|
||||
Now you can go through the [lifecycle operations](https://github.com/opencontainers/runtime-spec/blob/master/runtime.md)
|
||||
in your shell.
|
||||
You need to run `runk` as `root` because `runk` does not have the rootless feature which is the ability
|
||||
to run containers without root privileges.
|
||||
|
||||
```bash
|
||||
$ cd $bundle_dir
|
||||
|
||||
# Create a container
|
||||
$ sudo runk create test
|
||||
|
||||
# View the container is created and in the "created" state
|
||||
$ sudo runk state test
|
||||
|
||||
# Start the process inside the container
|
||||
$ sudo runk start test
|
||||
|
||||
# After 10 seconds view that the container has exited and is now in the "stopped" state
|
||||
$ sudo runk state test
|
||||
|
||||
# Now delete the container
|
||||
$ sudo runk delete test
|
||||
```
|
||||
|
||||
## Using `runk` from `containerd`
|
||||
|
||||
`runk` can run containers with the containerd runtime handler support on `containerd`.
|
||||
|
||||
### Prerequisites for `runk` with containerd
|
||||
|
||||
* `containerd` v1.2.4 or above
|
||||
* `cri-tool`
|
||||
|
||||
> **Note:**
|
||||
> [`cri-tools`](https://github.com/kubernetes-sigs/cri-tools) is a set of tools for CRI
|
||||
> used for development and testing.
|
||||
|
||||
Install `cri-tools` from source code:
|
||||
|
||||
```bash
|
||||
$ go get github.com/kubernetes-incubator/cri-tools
|
||||
$ pushd $GOPATH/src/github.com/kubernetes-incubator/cri-tools
|
||||
$ make
|
||||
$ sudo -E make install
|
||||
$ popd
|
||||
```
|
||||
|
||||
Write the `crictl` configuration file:
|
||||
|
||||
``` bash
|
||||
$ cat <<EOF | sudo tee /etc/crictl.yaml
|
||||
runtime-endpoint: unix:///run/containerd/containerd.sock
|
||||
EOF
|
||||
```
|
||||
|
||||
### Configure `containerd` to use `runk`
|
||||
|
||||
Update `/etc/containerd/config.toml`:
|
||||
|
||||
```bash
|
||||
$ cat <<EOF | sudo tee /etc/containerd/config.toml
|
||||
version = 2
|
||||
[plugins."io.containerd.runtime.v1.linux"]
|
||||
shim_debug = true
|
||||
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
|
||||
runtime_type = "io.containerd.runc.v2"
|
||||
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runk]
|
||||
runtime_type = "io.containerd.runc.v2"
|
||||
EOF
|
||||
```
|
||||
|
||||
Restart `containerd`:
|
||||
|
||||
```bash
|
||||
$ sudo systemctl restart containerd
|
||||
```
|
||||
|
||||
### Running a container with `crictl` command line
|
||||
|
||||
You can run containers in `runk` via containerd's CRI.
|
||||
|
||||
Pull the `busybox` image:
|
||||
|
||||
``` bash
|
||||
$ sudo crictl pull busybox
|
||||
```
|
||||
|
||||
Create the sandbox configuration:
|
||||
|
||||
``` bash
|
||||
$ cat <<EOF | tee sandbox.json
|
||||
{
|
||||
"metadata": {
|
||||
"name": "busybox-sandbox",
|
||||
"namespace": "default",
|
||||
"attempt": 1,
|
||||
"uid": "hdishd83djaidwnduwk28bcsb"
|
||||
},
|
||||
"log_directory": "/tmp",
|
||||
"linux": {
|
||||
}
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
Create the container configuration:
|
||||
|
||||
``` bash
|
||||
$ cat <<EOF | tee container.json
|
||||
{
|
||||
"metadata": {
|
||||
"name": "busybox"
|
||||
},
|
||||
"image": {
|
||||
"image": "docker.io/busybox"
|
||||
},
|
||||
"command": [
|
||||
"sh"
|
||||
],
|
||||
"envs": [
|
||||
{
|
||||
"key": "PATH",
|
||||
"value": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
},
|
||||
{
|
||||
"key": "TERM",
|
||||
"value": "xterm"
|
||||
}
|
||||
],
|
||||
"log_path": "busybox.0.log",
|
||||
"stdin": true,
|
||||
"stdin_once": true,
|
||||
"tty": true
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
With the `crictl` command line of `cri-tools`, you can specify runtime class with `-r` or `--runtime` flag.
|
||||
|
||||
Launch a sandbox and container using the `crictl`:
|
||||
|
||||
```bash
|
||||
# Run a container inside a sandbox
|
||||
$ sudo crictl run -r runk container.json sandbox.json
|
||||
f492eee753887ba3dfbba9022028975380739aba1269df431d097b73b23c3871
|
||||
|
||||
# Attach to the running container
|
||||
$ sudo crictl attach --stdin --tty f492eee753887ba3dfbba9022028975380739aba1269df431d097b73b23c3871
|
||||
/ #
|
||||
```
|
||||
|
23
src/tools/runk/libcontainer/Cargo.toml
Normal file
23
src/tools/runk/libcontainer/Cargo.toml
Normal file
@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "libcontainer"
|
||||
version = "0.0.1"
|
||||
authors = ["The Kata Containers community <kata-dev@lists.katacontainers.io>"]
|
||||
description = "Library for runk container"
|
||||
license = "Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
rustjail = { path = "../../../agent/rustjail", features = ["standard-oci-runtime"] }
|
||||
oci = { path = "../../../libs/oci" }
|
||||
logging = { path = "../../../libs/logging" }
|
||||
derive_builder = "0.10.2"
|
||||
libc = "0.2.108"
|
||||
nix = "0.23.0"
|
||||
anyhow = "1.0.52"
|
||||
slog = "2.7.0"
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
serde = { version = "1.0.133", features = ["derive"] }
|
||||
serde_json = "1.0.74"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.3.0"
|
121
src/tools/runk/libcontainer/src/builder.rs
Normal file
121
src/tools/runk/libcontainer/src/builder.rs
Normal file
@ -0,0 +1,121 @@
|
||||
// Copyright 2021-2022 Sony Group Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
use crate::container::{get_config_path, ContainerContext};
|
||||
use anyhow::{anyhow, Result};
|
||||
use derive_builder::Builder;
|
||||
use oci::Spec;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Default, Builder, Debug)]
|
||||
pub struct Container {
|
||||
id: String,
|
||||
bundle: PathBuf,
|
||||
root: PathBuf,
|
||||
console_socket: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Container {
|
||||
pub fn create_ctx(self) -> Result<ContainerContext> {
|
||||
let bundle_canon = self.bundle.canonicalize()?;
|
||||
let config_path = get_config_path(&bundle_canon);
|
||||
let mut spec = Spec::load(
|
||||
config_path
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("invalid config path"))?,
|
||||
)?;
|
||||
|
||||
if spec.root.is_some() {
|
||||
let mut spec_root = spec
|
||||
.root
|
||||
.as_mut()
|
||||
.ok_or_else(|| anyhow!("root config was not present in the spec file"))?;
|
||||
let rootfs_path = Path::new(&spec_root.path);
|
||||
|
||||
// If the rootfs path in the spec file is a relative path,
|
||||
// convert it into a canonical path to pass validation of rootfs in the agent.
|
||||
if !&rootfs_path.is_absolute() {
|
||||
let rootfs_name = rootfs_path
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("invalid rootfs name"))?;
|
||||
spec_root.path = bundle_canon
|
||||
.join(rootfs_name)
|
||||
.to_str()
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| anyhow!("failed to convert bundle path"))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ContainerContext {
|
||||
id: self.id,
|
||||
bundle: self.bundle,
|
||||
state_root: self.root,
|
||||
spec,
|
||||
// TODO: liboci-cli does not support --no-pivot option for create and run command.
|
||||
// After liboci-cli supports the option, we will change the following code.
|
||||
// no_pivot_root: self.no_pivot,
|
||||
no_pivot_root: false,
|
||||
console_socket: self.console_socket,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::container::CONFIG_FILE_NAME;
|
||||
use oci::Spec;
|
||||
use std::{fs::File, path::PathBuf};
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestData {
|
||||
id: String,
|
||||
bundle: PathBuf,
|
||||
root: PathBuf,
|
||||
console_socket: Option<PathBuf>,
|
||||
spec: Spec,
|
||||
no_pivot_root: bool,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_ctx() {
|
||||
let bundle_dir = tempdir().unwrap();
|
||||
let config_file = bundle_dir.path().join(CONFIG_FILE_NAME);
|
||||
let spec = Spec::default();
|
||||
let file = File::create(config_file).unwrap();
|
||||
serde_json::to_writer(&file, &spec).unwrap();
|
||||
|
||||
let test_data = TestData {
|
||||
id: String::from("test"),
|
||||
bundle: PathBuf::from(bundle_dir.into_path()),
|
||||
root: PathBuf::from("test"),
|
||||
console_socket: Some(PathBuf::from("test")),
|
||||
spec: Spec::default(),
|
||||
no_pivot_root: false,
|
||||
};
|
||||
|
||||
let test_ctx = ContainerContext {
|
||||
id: test_data.id.clone(),
|
||||
bundle: test_data.bundle.clone(),
|
||||
state_root: test_data.root.clone(),
|
||||
spec: test_data.spec.clone(),
|
||||
no_pivot_root: test_data.no_pivot_root,
|
||||
console_socket: test_data.console_socket.clone(),
|
||||
};
|
||||
|
||||
let ctx = ContainerBuilder::default()
|
||||
.id(test_data.id.clone())
|
||||
.bundle(test_data.bundle.clone())
|
||||
.root(test_data.root.clone())
|
||||
.console_socket(test_data.console_socket.clone())
|
||||
.build()
|
||||
.unwrap()
|
||||
.create_ctx()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(test_ctx, ctx);
|
||||
}
|
||||
}
|
40
src/tools/runk/libcontainer/src/cgroup.rs
Normal file
40
src/tools/runk/libcontainer/src/cgroup.rs
Normal file
@ -0,0 +1,40 @@
|
||||
// Copyright 2021-2022 Sony Group Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use rustjail::cgroups::fs::Manager as CgroupManager;
|
||||
use std::{
|
||||
path::Path,
|
||||
{fs, thread, time},
|
||||
};
|
||||
|
||||
pub fn destroy_cgroup(cgroup_mg: &CgroupManager) -> Result<()> {
|
||||
for path in cgroup_mg.paths.values() {
|
||||
remove_cgroup_dir(Path::new(path))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Try to remove the provided cgroups path five times with increasing delay between tries.
|
||||
// If after all there are not removed cgroups, an appropriate error will be returned.
|
||||
fn remove_cgroup_dir(path: &Path) -> Result<()> {
|
||||
let mut retries = 5;
|
||||
let mut delay = time::Duration::from_millis(10);
|
||||
while retries != 0 {
|
||||
if retries != 5 {
|
||||
delay *= 2;
|
||||
thread::sleep(delay);
|
||||
}
|
||||
|
||||
if !path.exists() || fs::remove_dir(path).is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
retries -= 1;
|
||||
}
|
||||
|
||||
return Err(anyhow!("failed to remove cgroups paths: {:?}", path));
|
||||
}
|
151
src/tools/runk/libcontainer/src/container.rs
Normal file
151
src/tools/runk/libcontainer/src/container.rs
Normal file
@ -0,0 +1,151 @@
|
||||
// Copyright 2021-2022 Sony Group Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
use crate::status::Status;
|
||||
use anyhow::{anyhow, Result};
|
||||
use nix::unistd::{chdir, unlink, Pid};
|
||||
use oci::Spec;
|
||||
use rustjail::{
|
||||
container::{BaseContainer, LinuxContainer, EXEC_FIFO_FILENAME},
|
||||
process::Process,
|
||||
specconv::CreateOpts,
|
||||
};
|
||||
use slog::Logger;
|
||||
use std::{
|
||||
env::current_dir,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
pub const CONFIG_FILE_NAME: &str = "config.json";
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum ContainerAction {
|
||||
Create,
|
||||
Run,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ContainerContext {
|
||||
pub id: String,
|
||||
pub bundle: PathBuf,
|
||||
pub state_root: PathBuf,
|
||||
pub spec: Spec,
|
||||
pub no_pivot_root: bool,
|
||||
pub console_socket: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl ContainerContext {
|
||||
pub async fn launch(&self, action: ContainerAction, logger: &Logger) -> Result<Pid> {
|
||||
Status::create_dir(&self.state_root, &self.id)?;
|
||||
|
||||
let current_dir = current_dir()?;
|
||||
chdir(&self.bundle)?;
|
||||
|
||||
let create_opts = CreateOpts {
|
||||
cgroup_name: "".to_string(),
|
||||
use_systemd_cgroup: false,
|
||||
no_pivot_root: self.no_pivot_root,
|
||||
no_new_keyring: false,
|
||||
spec: Some(self.spec.clone()),
|
||||
rootless_euid: false,
|
||||
rootless_cgroup: false,
|
||||
};
|
||||
|
||||
let mut ctr = LinuxContainer::new(
|
||||
&self.id,
|
||||
&self
|
||||
.state_root
|
||||
.to_str()
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| anyhow!("failed to convert bundle path"))?,
|
||||
create_opts.clone(),
|
||||
logger,
|
||||
)?;
|
||||
|
||||
let process = if self.spec.process.is_some() {
|
||||
Process::new(
|
||||
logger,
|
||||
self.spec
|
||||
.process
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("process config was not present in the spec file"))?,
|
||||
&self.id,
|
||||
true,
|
||||
0,
|
||||
)?
|
||||
} else {
|
||||
return Err(anyhow!("no process configuration"));
|
||||
};
|
||||
|
||||
if let Some(ref csocket_path) = self.console_socket {
|
||||
ctr.set_console_socket(csocket_path)?;
|
||||
}
|
||||
|
||||
match action {
|
||||
ContainerAction::Create => {
|
||||
ctr.start(process).await?;
|
||||
}
|
||||
ContainerAction::Run => {
|
||||
ctr.run(process).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let oci_state = ctr.oci_state()?;
|
||||
let status = Status::new(
|
||||
&self.state_root,
|
||||
oci_state,
|
||||
ctr.init_process_start_time,
|
||||
ctr.created,
|
||||
ctr.cgroup_manager
|
||||
.ok_or_else(|| anyhow!("cgroup manager was not present"))?,
|
||||
create_opts,
|
||||
)?;
|
||||
|
||||
status.save()?;
|
||||
|
||||
if action == ContainerAction::Run {
|
||||
let fifo_path = get_fifo_path(&status);
|
||||
if fifo_path.exists() {
|
||||
unlink(&fifo_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
chdir(¤t_dir)?;
|
||||
|
||||
Ok(Pid::from_raw(ctr.init_process_pid))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_config_path<P: AsRef<Path>>(bundle: P) -> PathBuf {
|
||||
bundle.as_ref().join(CONFIG_FILE_NAME)
|
||||
}
|
||||
|
||||
pub fn get_fifo_path(status: &Status) -> PathBuf {
|
||||
status.root.join(&status.id).join(EXEC_FIFO_FILENAME)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::utils::test_utils::*;
|
||||
use rustjail::container::EXEC_FIFO_FILENAME;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn test_get_config_path() {
|
||||
let test_data = PathBuf::from(TEST_BUNDLE_PATH).join(CONFIG_FILE_NAME);
|
||||
assert_eq!(get_config_path(TEST_BUNDLE_PATH), test_data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_fifo_path() {
|
||||
let test_data = PathBuf::from(TEST_BUNDLE_PATH)
|
||||
.join(TEST_CONTAINER_ID)
|
||||
.join(EXEC_FIFO_FILENAME);
|
||||
let status = create_dummy_status();
|
||||
|
||||
assert_eq!(get_fifo_path(&status), test_data);
|
||||
}
|
||||
}
|
10
src/tools/runk/libcontainer/src/lib.rs
Normal file
10
src/tools/runk/libcontainer/src/lib.rs
Normal file
@ -0,0 +1,10 @@
|
||||
// Copyright 2021-2022 Sony Group Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
pub mod builder;
|
||||
pub mod cgroup;
|
||||
pub mod container;
|
||||
pub mod status;
|
||||
pub mod utils;
|
246
src/tools/runk/libcontainer/src/status.rs
Normal file
246
src/tools/runk/libcontainer/src/status.rs
Normal file
@ -0,0 +1,246 @@
|
||||
// Copyright 2021-2022 Sony Group Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
use crate::container::get_fifo_path;
|
||||
use crate::utils::*;
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use libc::pid_t;
|
||||
use nix::{
|
||||
errno::Errno,
|
||||
sys::{signal::kill, stat::Mode},
|
||||
unistd::Pid,
|
||||
};
|
||||
use oci::{ContainerState, State as OCIState};
|
||||
use rustjail::{cgroups::fs::Manager as CgroupManager, specconv::CreateOpts};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fs::{self, File, OpenOptions},
|
||||
path::{Path, PathBuf},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
const STATUS_FILE: &str = "status.json";
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Status {
|
||||
pub oci_version: String,
|
||||
pub id: String,
|
||||
pub pid: pid_t,
|
||||
pub root: PathBuf,
|
||||
pub bundle: PathBuf,
|
||||
pub rootfs: String,
|
||||
pub process_start_time: u64,
|
||||
pub created: DateTime<Utc>,
|
||||
pub cgroup_manager: CgroupManager,
|
||||
pub config: CreateOpts,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
pub fn new(
|
||||
root: &Path,
|
||||
oci_state: OCIState,
|
||||
process_start_time: u64,
|
||||
created_time: SystemTime,
|
||||
cgroup_mg: CgroupManager,
|
||||
config: CreateOpts,
|
||||
) -> Result<Self> {
|
||||
let created = DateTime::from(created_time);
|
||||
let rootfs = config
|
||||
.clone()
|
||||
.spec
|
||||
.ok_or_else(|| anyhow!("spec config was not present"))?
|
||||
.root
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("root config was not present in the spec"))?
|
||||
.path
|
||||
.clone();
|
||||
|
||||
Ok(Self {
|
||||
oci_version: oci_state.version,
|
||||
id: oci_state.id,
|
||||
pid: oci_state.pid,
|
||||
root: root.to_path_buf(),
|
||||
bundle: PathBuf::from(&oci_state.bundle),
|
||||
rootfs,
|
||||
process_start_time,
|
||||
created,
|
||||
cgroup_manager: cgroup_mg,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn save(&self) -> Result<()> {
|
||||
let state_file_path = Self::get_file_path(&self.root, &self.id);
|
||||
|
||||
if !&self.root.exists() {
|
||||
create_dir_with_mode(&self.root, Mode::S_IRWXU, true)?;
|
||||
}
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(state_file_path)?;
|
||||
|
||||
serde_json::to_writer(&file, self)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load(state_root: &Path, id: &str) -> Result<Self> {
|
||||
let state_file_path = Self::get_file_path(state_root, id);
|
||||
if !state_file_path.exists() {
|
||||
return Err(anyhow!("container \"{}\" does not exist", id));
|
||||
}
|
||||
|
||||
let file = File::open(&state_file_path)?;
|
||||
let state: Self = serde_json::from_reader(&file)?;
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
pub fn create_dir(state_root: &Path, id: &str) -> Result<()> {
|
||||
let state_dir_path = Self::get_dir_path(state_root, id);
|
||||
if !state_dir_path.exists() {
|
||||
create_dir_with_mode(state_dir_path, Mode::S_IRWXU, true)?;
|
||||
} else {
|
||||
return Err(anyhow!("container with id exists: \"{}\"", id));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_dir(&self) -> Result<()> {
|
||||
let state_dir_path = Self::get_dir_path(&self.root, &self.id);
|
||||
fs::remove_dir_all(state_dir_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_dir_path(state_root: &Path, id: &str) -> PathBuf {
|
||||
state_root.join(id)
|
||||
}
|
||||
|
||||
pub fn get_file_path(state_root: &Path, id: &str) -> PathBuf {
|
||||
state_root.join(id).join(STATUS_FILE)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_process_running(pid: Pid) -> Result<bool> {
|
||||
match kill(pid, None) {
|
||||
Err(errno) => {
|
||||
if errno != Errno::ESRCH {
|
||||
return Err(anyhow!("no such process"));
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
Ok(()) => Ok(true),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_current_container_state(status: &Status) -> Result<ContainerState> {
|
||||
let running = is_process_running(Pid::from_raw(status.pid))?;
|
||||
let mut has_fifo = false;
|
||||
|
||||
if running {
|
||||
let fifo = get_fifo_path(status);
|
||||
if fifo.exists() {
|
||||
has_fifo = true
|
||||
}
|
||||
}
|
||||
|
||||
if running && !has_fifo {
|
||||
// TODO: Check paused status.
|
||||
// runk does not support pause command currently.
|
||||
}
|
||||
|
||||
if !running {
|
||||
Ok(ContainerState::Stopped)
|
||||
} else if has_fifo {
|
||||
Ok(ContainerState::Created)
|
||||
} else {
|
||||
Ok(ContainerState::Running)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_all_pid(cgm: &CgroupManager) -> Result<Vec<Pid>> {
|
||||
let cgroup_path = cgm.paths.get("devices");
|
||||
match cgroup_path {
|
||||
Some(v) => {
|
||||
let path = Path::new(v);
|
||||
if !path.exists() {
|
||||
return Err(anyhow!("cgroup devices file does not exist"));
|
||||
}
|
||||
|
||||
let procs_path = path.join("cgroup.procs");
|
||||
let pids: Vec<Pid> = lines_from_file(&procs_path)?
|
||||
.into_iter()
|
||||
.map(|v| {
|
||||
Pid::from_raw(
|
||||
v.parse::<pid_t>()
|
||||
.expect("failed to parse string into pid_t"),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
Ok(pids)
|
||||
}
|
||||
None => Err(anyhow!("cgroup devices file dose not exist")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::utils::test_utils::*;
|
||||
use chrono::{DateTime, Utc};
|
||||
use nix::unistd::getpid;
|
||||
use oci::ContainerState;
|
||||
use rustjail::cgroups::fs::Manager as CgroupManager;
|
||||
use std::path::Path;
|
||||
use std::time::SystemTime;
|
||||
|
||||
#[test]
|
||||
fn test_status() {
|
||||
let cgm: CgroupManager = serde_json::from_str(TEST_CGM_DATA).unwrap();
|
||||
let oci_state = create_dummy_oci_state();
|
||||
let created = SystemTime::now();
|
||||
let status = Status::new(
|
||||
Path::new(TEST_BUNDLE_PATH),
|
||||
oci_state.clone(),
|
||||
1,
|
||||
created,
|
||||
cgm,
|
||||
create_dummy_opts(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(status.id, oci_state.id);
|
||||
assert_eq!(status.pid, oci_state.pid);
|
||||
assert_eq!(status.process_start_time, 1);
|
||||
assert_eq!(status.created, DateTime::<Utc>::from(created));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_process_running() {
|
||||
let pid = getpid();
|
||||
let ret = is_process_running(pid).unwrap();
|
||||
assert!(ret);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_current_container_state() {
|
||||
let status = create_dummy_status();
|
||||
let state = get_current_container_state(&status).unwrap();
|
||||
assert_eq!(state, ContainerState::Running);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_all_pid() {
|
||||
let cgm: CgroupManager = serde_json::from_str(TEST_CGM_DATA).unwrap();
|
||||
assert!(get_all_pid(&cgm).is_ok());
|
||||
}
|
||||
}
|
106
src/tools/runk/libcontainer/src/utils.rs
Normal file
106
src/tools/runk/libcontainer/src/utils.rs
Normal file
@ -0,0 +1,106 @@
|
||||
// Copyright 2021-2022 Sony Group Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use nix::sys::stat::Mode;
|
||||
use std::{
|
||||
fs::{DirBuilder, File},
|
||||
io::{prelude::*, BufReader},
|
||||
os::unix::fs::DirBuilderExt,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
pub fn lines_from_file<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
let file = File::open(&path)?;
|
||||
let buf = BufReader::new(file);
|
||||
Ok(buf
|
||||
.lines()
|
||||
.map(|v| v.expect("could not parse line"))
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn create_dir_with_mode<P: AsRef<Path>>(path: P, mode: Mode, recursive: bool) -> Result<()> {
|
||||
let path = path.as_ref();
|
||||
if path.exists() {
|
||||
return Err(anyhow!("{} already exists", path.display()));
|
||||
}
|
||||
|
||||
Ok(DirBuilder::new()
|
||||
.recursive(recursive)
|
||||
.mode(mode.bits())
|
||||
.create(path)?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test_utils {
|
||||
use crate::status::Status;
|
||||
use nix::unistd::getpid;
|
||||
use oci::State as OCIState;
|
||||
use oci::{ContainerState, Root, Spec};
|
||||
use rustjail::cgroups::fs::Manager as CgroupManager;
|
||||
use rustjail::specconv::CreateOpts;
|
||||
use std::path::Path;
|
||||
use std::time::SystemTime;
|
||||
|
||||
pub const TEST_CONTAINER_ID: &str = "test";
|
||||
pub const TEST_BUNDLE_PATH: &str = "/test";
|
||||
pub const TEST_ANNOTATION: &str = "test";
|
||||
pub const TEST_CGM_DATA: &str = r#"{
|
||||
"paths": {
|
||||
"devices": "/sys/fs/cgroup/devices"
|
||||
},
|
||||
"mounts": {
|
||||
"devices": "/sys/fs/cgroup/devices"
|
||||
},
|
||||
"cpath": "test"
|
||||
}"#;
|
||||
|
||||
pub fn create_dummy_opts() -> CreateOpts {
|
||||
let spec = Spec {
|
||||
root: Some(Root::default()),
|
||||
..Default::default()
|
||||
};
|
||||
CreateOpts {
|
||||
cgroup_name: "".to_string(),
|
||||
use_systemd_cgroup: false,
|
||||
no_pivot_root: false,
|
||||
no_new_keyring: false,
|
||||
spec: Some(spec),
|
||||
rootless_euid: false,
|
||||
rootless_cgroup: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_dummy_oci_state() -> OCIState {
|
||||
OCIState {
|
||||
version: "1.0.0".to_string(),
|
||||
id: TEST_CONTAINER_ID.to_string(),
|
||||
status: ContainerState::Running,
|
||||
pid: getpid().as_raw(),
|
||||
bundle: TEST_BUNDLE_PATH.to_string(),
|
||||
annotations: [(TEST_ANNOTATION.to_string(), TEST_ANNOTATION.to_string())]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_dummy_status() -> Status {
|
||||
let cgm: CgroupManager = serde_json::from_str(TEST_CGM_DATA).unwrap();
|
||||
let oci_state = create_dummy_oci_state();
|
||||
let created = SystemTime::now();
|
||||
let status = Status::new(
|
||||
Path::new(TEST_BUNDLE_PATH),
|
||||
oci_state.clone(),
|
||||
1,
|
||||
created,
|
||||
cgm,
|
||||
create_dummy_opts(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
status
|
||||
}
|
||||
}
|
37
src/tools/runk/src/commands/create.rs
Normal file
37
src/tools/runk/src/commands/create.rs
Normal file
@ -0,0 +1,37 @@
|
||||
// Copyright 2021-2022 Sony Group Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
use anyhow::Result;
|
||||
use libcontainer::{builder::ContainerBuilder, container::ContainerAction};
|
||||
use liboci_cli::Create;
|
||||
use nix::unistd::Pid;
|
||||
use slog::{info, Logger};
|
||||
use std::{fs, path::Path};
|
||||
|
||||
pub async fn run(opts: Create, root: &Path, logger: &Logger) -> Result<()> {
|
||||
let ctx = ContainerBuilder::default()
|
||||
.id(opts.container_id)
|
||||
.bundle(opts.bundle)
|
||||
.root(root.to_path_buf())
|
||||
.console_socket(opts.console_socket)
|
||||
.build()?
|
||||
.create_ctx()?;
|
||||
|
||||
let pid = ctx.launch(ContainerAction::Create, logger).await?;
|
||||
|
||||
if let Some(ref pid_file) = opts.pid_file {
|
||||
create_pid_file(pid_file, pid)?;
|
||||
}
|
||||
|
||||
info!(&logger, "create command finished successfully");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_pid_file<P: AsRef<Path>>(pid_file: P, pid: Pid) -> Result<()> {
|
||||
fs::write(pid_file.as_ref(), format!("{}", pid))?;
|
||||
|
||||
Ok(())
|
||||
}
|
103
src/tools/runk/src/commands/delete.rs
Normal file
103
src/tools/runk/src/commands/delete.rs
Normal file
@ -0,0 +1,103 @@
|
||||
// Copyright 2021-2022 Sony Group Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use libcontainer::{
|
||||
cgroup,
|
||||
status::{get_current_container_state, Status},
|
||||
};
|
||||
use liboci_cli::Delete;
|
||||
use nix::{
|
||||
errno::Errno,
|
||||
sys::signal::{kill, Signal},
|
||||
unistd::Pid,
|
||||
};
|
||||
use oci::{ContainerState, State as OCIState};
|
||||
use rustjail::container;
|
||||
use slog::{info, Logger};
|
||||
use std::{fs, path::Path};
|
||||
|
||||
pub async fn run(opts: Delete, root: &Path, logger: &Logger) -> Result<()> {
|
||||
let container_id = &opts.container_id;
|
||||
let status_dir = Status::get_dir_path(root, container_id);
|
||||
if !status_dir.exists() {
|
||||
return Err(anyhow!("container {} does not exist", container_id));
|
||||
}
|
||||
|
||||
let status = if let Ok(value) = Status::load(root, container_id) {
|
||||
value
|
||||
} else {
|
||||
fs::remove_dir_all(status_dir)?;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let spec = status
|
||||
.config
|
||||
.spec
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("spec config was not present in the status"))?;
|
||||
|
||||
let oci_state = OCIState {
|
||||
version: status.oci_version.clone(),
|
||||
id: status.id.clone(),
|
||||
status: get_current_container_state(&status)?,
|
||||
pid: status.pid,
|
||||
bundle: status
|
||||
.bundle
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("invalid bundle path"))?
|
||||
.to_string(),
|
||||
annotations: spec.annotations.clone(),
|
||||
};
|
||||
|
||||
if spec.hooks.is_some() {
|
||||
let hooks = spec
|
||||
.hooks
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("hooks config was not present"))?;
|
||||
for h in hooks.poststop.iter() {
|
||||
container::execute_hook(logger, h, &oci_state).await?;
|
||||
}
|
||||
}
|
||||
|
||||
match oci_state.status {
|
||||
ContainerState::Stopped => {
|
||||
destroy_container(&status)?;
|
||||
}
|
||||
ContainerState::Created => {
|
||||
kill(Pid::from_raw(status.pid), Some(Signal::SIGKILL))?;
|
||||
destroy_container(&status)?;
|
||||
}
|
||||
_ => {
|
||||
if opts.force {
|
||||
match kill(Pid::from_raw(status.pid), Some(Signal::SIGKILL)) {
|
||||
Err(errno) => {
|
||||
if errno != Errno::ESRCH {
|
||||
return Err(anyhow!("{}", errno));
|
||||
}
|
||||
}
|
||||
Ok(()) => {}
|
||||
}
|
||||
destroy_container(&status)?;
|
||||
} else {
|
||||
return Err(anyhow!(
|
||||
"cannot delete container {} that is not stopped",
|
||||
container_id
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!(&logger, "delete command finished successfully");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn destroy_container(status: &Status) -> Result<()> {
|
||||
cgroup::destroy_cgroup(&status.cgroup_manager)?;
|
||||
status.remove_dir()?;
|
||||
|
||||
Ok(())
|
||||
}
|
82
src/tools/runk/src/commands/kill.rs
Normal file
82
src/tools/runk/src/commands/kill.rs
Normal file
@ -0,0 +1,82 @@
|
||||
// Copyright 2021-2022 Sony Group Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use libcontainer::status::{self, get_current_container_state, Status};
|
||||
use liboci_cli::Kill;
|
||||
use nix::{
|
||||
sys::signal::{kill, Signal},
|
||||
unistd::Pid,
|
||||
};
|
||||
use oci::ContainerState;
|
||||
use slog::{info, Logger};
|
||||
use std::{convert::TryFrom, path::Path, str::FromStr};
|
||||
|
||||
pub fn run(opts: Kill, state_root: &Path, logger: &Logger) -> Result<()> {
|
||||
let container_id = &opts.container_id;
|
||||
let status = Status::load(state_root, container_id)?;
|
||||
let current_state = get_current_container_state(&status)?;
|
||||
let sig = parse_signal(&opts.signal)?;
|
||||
|
||||
// TODO: liboci-cli does not support --all option for kill command.
|
||||
// After liboci-cli supports the option, we will change the following code.
|
||||
let all = false;
|
||||
if all {
|
||||
let pids = status::get_all_pid(&status.cgroup_manager)?;
|
||||
for pid in pids {
|
||||
if !status::is_process_running(pid)? {
|
||||
continue;
|
||||
}
|
||||
kill(pid, sig)?;
|
||||
}
|
||||
} else {
|
||||
if current_state == ContainerState::Stopped {
|
||||
return Err(anyhow!("container {} not running", container_id));
|
||||
}
|
||||
|
||||
let p = Pid::from_raw(status.pid);
|
||||
if status::is_process_running(p)? {
|
||||
kill(p, sig)?;
|
||||
}
|
||||
}
|
||||
|
||||
info!(&logger, "kill command finished successfully");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_signal(signal: &str) -> Result<Signal> {
|
||||
if let Ok(num) = signal.parse::<i32>() {
|
||||
return Ok(Signal::try_from(num)?);
|
||||
}
|
||||
|
||||
let mut signal_upper = signal.to_uppercase();
|
||||
if !signal_upper.starts_with("SIG") {
|
||||
signal_upper = "SIG".to_string() + &signal_upper;
|
||||
}
|
||||
|
||||
Ok(Signal::from_str(&signal_upper)?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use nix::sys::signal::Signal;
|
||||
|
||||
#[test]
|
||||
fn test_parse_signal() {
|
||||
assert_eq!(Signal::SIGHUP, parse_signal("1").unwrap());
|
||||
assert_eq!(Signal::SIGHUP, parse_signal("sighup").unwrap());
|
||||
assert_eq!(Signal::SIGHUP, parse_signal("hup").unwrap());
|
||||
assert_eq!(Signal::SIGHUP, parse_signal("SIGHUP").unwrap());
|
||||
assert_eq!(Signal::SIGHUP, parse_signal("HUP").unwrap());
|
||||
|
||||
assert_eq!(Signal::SIGKILL, parse_signal("9").unwrap());
|
||||
assert_eq!(Signal::SIGKILL, parse_signal("sigkill").unwrap());
|
||||
assert_eq!(Signal::SIGKILL, parse_signal("kill").unwrap());
|
||||
assert_eq!(Signal::SIGKILL, parse_signal("SIGKILL").unwrap());
|
||||
assert_eq!(Signal::SIGKILL, parse_signal("KILL").unwrap());
|
||||
}
|
||||
}
|
12
src/tools/runk/src/commands/mod.rs
Normal file
12
src/tools/runk/src/commands/mod.rs
Normal file
@ -0,0 +1,12 @@
|
||||
// Copyright 2021-2022 Sony Group Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
pub mod create;
|
||||
pub mod delete;
|
||||
pub mod kill;
|
||||
pub mod run;
|
||||
pub mod spec;
|
||||
pub mod start;
|
||||
pub mod state;
|
26
src/tools/runk/src/commands/run.rs
Normal file
26
src/tools/runk/src/commands/run.rs
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright 2021-2022 Sony Group Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
use anyhow::Result;
|
||||
use libcontainer::{builder::ContainerBuilder, container::ContainerAction};
|
||||
use liboci_cli::Run;
|
||||
use slog::{info, Logger};
|
||||
use std::path::Path;
|
||||
|
||||
pub async fn run(opts: Run, root: &Path, logger: &Logger) -> Result<()> {
|
||||
let ctx = ContainerBuilder::default()
|
||||
.id(opts.container_id)
|
||||
.bundle(opts.bundle)
|
||||
.root(root.to_path_buf())
|
||||
.console_socket(opts.console_socket)
|
||||
.build()?
|
||||
.create_ctx()?;
|
||||
|
||||
ctx.launch(ContainerAction::Run, logger).await?;
|
||||
|
||||
info!(&logger, "run command finished successfully");
|
||||
|
||||
Ok(())
|
||||
}
|
207
src/tools/runk/src/commands/spec.rs
Normal file
207
src/tools/runk/src/commands/spec.rs
Normal file
@ -0,0 +1,207 @@
|
||||
// Copyright 2021-2022 Sony Group Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
//use crate::container::get_config_path;
|
||||
use anyhow::Result;
|
||||
use libcontainer::container::CONFIG_FILE_NAME;
|
||||
use liboci_cli::Spec;
|
||||
use slog::{info, Logger};
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
|
||||
pub const DEFAULT_SPEC: &str = r#"{
|
||||
"ociVersion": "1.0.2-dev",
|
||||
"process": {
|
||||
"terminal": true,
|
||||
"user": {
|
||||
"uid": 0,
|
||||
"gid": 0
|
||||
},
|
||||
"args": [
|
||||
"sh"
|
||||
],
|
||||
"env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"TERM=xterm"
|
||||
],
|
||||
"cwd": "/",
|
||||
"capabilities": {
|
||||
"bounding": [
|
||||
"CAP_AUDIT_WRITE",
|
||||
"CAP_KILL",
|
||||
"CAP_NET_BIND_SERVICE"
|
||||
],
|
||||
"effective": [
|
||||
"CAP_AUDIT_WRITE",
|
||||
"CAP_KILL",
|
||||
"CAP_NET_BIND_SERVICE"
|
||||
],
|
||||
"inheritable": [
|
||||
"CAP_AUDIT_WRITE",
|
||||
"CAP_KILL",
|
||||
"CAP_NET_BIND_SERVICE"
|
||||
],
|
||||
"permitted": [
|
||||
"CAP_AUDIT_WRITE",
|
||||
"CAP_KILL",
|
||||
"CAP_NET_BIND_SERVICE"
|
||||
],
|
||||
"ambient": [
|
||||
"CAP_AUDIT_WRITE",
|
||||
"CAP_KILL",
|
||||
"CAP_NET_BIND_SERVICE"
|
||||
]
|
||||
},
|
||||
"rlimits": [
|
||||
{
|
||||
"type": "RLIMIT_NOFILE",
|
||||
"hard": 1024,
|
||||
"soft": 1024
|
||||
}
|
||||
],
|
||||
"noNewPrivileges": true
|
||||
},
|
||||
"root": {
|
||||
"path": "rootfs",
|
||||
"readonly": true
|
||||
},
|
||||
"hostname": "runk",
|
||||
"mounts": [
|
||||
{
|
||||
"destination": "/proc",
|
||||
"type": "proc",
|
||||
"source": "proc"
|
||||
},
|
||||
{
|
||||
"destination": "/dev",
|
||||
"type": "tmpfs",
|
||||
"source": "tmpfs",
|
||||
"options": [
|
||||
"nosuid",
|
||||
"strictatime",
|
||||
"mode=755",
|
||||
"size=65536k"
|
||||
]
|
||||
},
|
||||
{
|
||||
"destination": "/dev/pts",
|
||||
"type": "devpts",
|
||||
"source": "devpts",
|
||||
"options": [
|
||||
"nosuid",
|
||||
"noexec",
|
||||
"newinstance",
|
||||
"ptmxmode=0666",
|
||||
"mode=0620",
|
||||
"gid=5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"destination": "/dev/shm",
|
||||
"type": "tmpfs",
|
||||
"source": "shm",
|
||||
"options": [
|
||||
"nosuid",
|
||||
"noexec",
|
||||
"nodev",
|
||||
"mode=1777",
|
||||
"size=65536k"
|
||||
]
|
||||
},
|
||||
{
|
||||
"destination": "/dev/mqueue",
|
||||
"type": "mqueue",
|
||||
"source": "mqueue",
|
||||
"options": [
|
||||
"nosuid",
|
||||
"noexec",
|
||||
"nodev"
|
||||
]
|
||||
},
|
||||
{
|
||||
"destination": "/sys",
|
||||
"type": "sysfs",
|
||||
"source": "sysfs",
|
||||
"options": [
|
||||
"nosuid",
|
||||
"noexec",
|
||||
"nodev",
|
||||
"ro"
|
||||
]
|
||||
},
|
||||
{
|
||||
"destination": "/sys/fs/cgroup",
|
||||
"type": "cgroup",
|
||||
"source": "cgroup",
|
||||
"options": [
|
||||
"nosuid",
|
||||
"noexec",
|
||||
"nodev",
|
||||
"relatime",
|
||||
"ro"
|
||||
]
|
||||
}
|
||||
],
|
||||
"linux": {
|
||||
"resources": {
|
||||
"devices": [
|
||||
{
|
||||
"allow": false,
|
||||
"access": "rwm"
|
||||
}
|
||||
]
|
||||
},
|
||||
"namespaces": [
|
||||
{
|
||||
"type": "pid"
|
||||
},
|
||||
{
|
||||
"type": "network"
|
||||
},
|
||||
{
|
||||
"type": "ipc"
|
||||
},
|
||||
{
|
||||
"type": "uts"
|
||||
},
|
||||
{
|
||||
"type": "mount"
|
||||
}
|
||||
],
|
||||
"maskedPaths": [
|
||||
"/proc/acpi",
|
||||
"/proc/asound",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/sys/firmware",
|
||||
"/proc/scsi"
|
||||
],
|
||||
"readonlyPaths": [
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
pub fn run(_opts: Spec, logger: &Logger) -> Result<()> {
|
||||
// TODO: liboci-cli does not support --bundle option for spec command.
|
||||
// After liboci-cli supports the option, we will change the following code.
|
||||
// let config_path = get_config_path(&opts.bundle);
|
||||
let config_path = Path::new(".").join(CONFIG_FILE_NAME);
|
||||
let config_data = DEFAULT_SPEC;
|
||||
|
||||
let mut file = File::create(config_path)?;
|
||||
file.write_all(config_data.as_bytes())?;
|
||||
|
||||
info!(&logger, "spec command finished successfully");
|
||||
|
||||
Ok(())
|
||||
}
|
48
src/tools/runk/src/commands/start.rs
Normal file
48
src/tools/runk/src/commands/start.rs
Normal file
@ -0,0 +1,48 @@
|
||||
// Copyright 2021-2022 Sony Group Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
use crate::commands::state::get_container_state_name;
|
||||
use anyhow::{anyhow, Result};
|
||||
use libcontainer::{
|
||||
container::get_fifo_path,
|
||||
status::{get_current_container_state, Status},
|
||||
};
|
||||
use liboci_cli::Start;
|
||||
use nix::unistd::unlink;
|
||||
use oci::ContainerState;
|
||||
use slog::{info, Logger};
|
||||
use std::{fs::OpenOptions, io::prelude::*, path::Path, time::SystemTime};
|
||||
|
||||
pub fn run(opts: Start, state_root: &Path, logger: &Logger) -> Result<()> {
|
||||
let mut status = Status::load(state_root, &opts.container_id)?;
|
||||
let state = get_current_container_state(&status)?;
|
||||
if state != ContainerState::Created {
|
||||
return Err(anyhow!(
|
||||
"cannot start a container in the {} state",
|
||||
get_container_state_name(state)
|
||||
));
|
||||
};
|
||||
|
||||
let fifo_path = get_fifo_path(&status);
|
||||
let mut file = OpenOptions::new().write(true).open(&fifo_path)?;
|
||||
|
||||
file.write_all("0".as_bytes())?;
|
||||
|
||||
info!(&logger, "container started");
|
||||
|
||||
status.process_start_time = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)?
|
||||
.as_secs();
|
||||
|
||||
status.save()?;
|
||||
|
||||
if fifo_path.exists() {
|
||||
unlink(&fifo_path)?;
|
||||
}
|
||||
|
||||
info!(&logger, "start command finished successfully");
|
||||
|
||||
Ok(())
|
||||
}
|
79
src/tools/runk/src/commands/state.rs
Normal file
79
src/tools/runk/src/commands/state.rs
Normal file
@ -0,0 +1,79 @@
|
||||
// Copyright 2021-2022 Sony Group Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, Utc};
|
||||
use libcontainer::status::{get_current_container_state, Status};
|
||||
use liboci_cli::State;
|
||||
use oci::ContainerState;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use slog::{info, Logger};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeState {
|
||||
pub oci_version: String,
|
||||
pub id: String,
|
||||
pub pid: i32,
|
||||
pub status: String,
|
||||
pub bundle: PathBuf,
|
||||
pub created: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl RuntimeState {
|
||||
pub fn new(status: Status, state: ContainerState) -> Self {
|
||||
Self {
|
||||
oci_version: status.oci_version,
|
||||
id: status.id,
|
||||
pid: status.pid,
|
||||
status: get_container_state_name(state),
|
||||
bundle: status.bundle,
|
||||
created: status.created,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(opts: State, state_root: &Path, logger: &Logger) -> Result<()> {
|
||||
let status = Status::load(state_root, &opts.container_id)?;
|
||||
let state = get_current_container_state(&status)?;
|
||||
let oci_state = RuntimeState::new(status, state);
|
||||
let json_state = &serde_json::to_string_pretty(&oci_state)?;
|
||||
|
||||
println!("{}", json_state);
|
||||
|
||||
info!(&logger, "state command finished successfully");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_container_state_name(state: ContainerState) -> String {
|
||||
match state {
|
||||
ContainerState::Creating => "creating",
|
||||
ContainerState::Created => "created",
|
||||
ContainerState::Running => "running",
|
||||
ContainerState::Stopped => "stopped",
|
||||
ContainerState::Paused => "paused",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use oci::ContainerState;
|
||||
|
||||
#[test]
|
||||
fn test_get_container_state_name() {
|
||||
assert_eq!(
|
||||
"creating",
|
||||
get_container_state_name(ContainerState::Creating)
|
||||
);
|
||||
assert_eq!("created", get_container_state_name(ContainerState::Created));
|
||||
assert_eq!("running", get_container_state_name(ContainerState::Running));
|
||||
assert_eq!("stopped", get_container_state_name(ContainerState::Stopped));
|
||||
assert_eq!("paused", get_container_state_name(ContainerState::Paused));
|
||||
}
|
||||
}
|
111
src/tools/runk/src/main.rs
Normal file
111
src/tools/runk/src/main.rs
Normal file
@ -0,0 +1,111 @@
|
||||
// Copyright 2021-2022 Sony Group Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::{crate_description, crate_name, Parser};
|
||||
use liboci_cli::{CommonCmd, GlobalOpts, StandardCmd};
|
||||
use slog::{o, Logger};
|
||||
use slog_async::AsyncGuard;
|
||||
use std::{
|
||||
fs::OpenOptions,
|
||||
path::{Path, PathBuf},
|
||||
process::exit,
|
||||
};
|
||||
|
||||
const DEFAULT_ROOT_DIR: &str = "/run/runk";
|
||||
const DEFAULT_LOG_LEVEL: slog::Level = slog::Level::Info;
|
||||
|
||||
mod commands;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
enum SubCommand {
|
||||
#[clap(flatten)]
|
||||
Standard(StandardCmd),
|
||||
#[clap(flatten)]
|
||||
Common(CommonCmd),
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(version, author, about = crate_description!())]
|
||||
struct Cli {
|
||||
#[clap(flatten)]
|
||||
global: GlobalOpts,
|
||||
#[clap(subcommand)]
|
||||
subcmd: SubCommand,
|
||||
}
|
||||
|
||||
async fn cmd_run(subcmd: SubCommand, root_path: &Path, logger: &Logger) -> Result<()> {
|
||||
match subcmd {
|
||||
SubCommand::Standard(cmd) => match cmd {
|
||||
StandardCmd::Create(create) => commands::create::run(create, root_path, logger).await,
|
||||
StandardCmd::Start(start) => commands::start::run(start, root_path, logger),
|
||||
StandardCmd::Kill(kill) => commands::kill::run(kill, root_path, logger),
|
||||
StandardCmd::Delete(delete) => commands::delete::run(delete, root_path, logger).await,
|
||||
StandardCmd::State(state) => commands::state::run(state, root_path, logger),
|
||||
},
|
||||
SubCommand::Common(cmd) => match cmd {
|
||||
CommonCmd::Run(run) => commands::run::run(run, root_path, logger).await,
|
||||
CommonCmd::Spec(spec) => commands::spec::run(spec, logger),
|
||||
_ => {
|
||||
return Err(anyhow!("command is not implemented yet"));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_logger(
|
||||
log_file: Option<PathBuf>,
|
||||
log_level: slog::Level,
|
||||
) -> Result<(Logger, Option<AsyncGuard>)> {
|
||||
if let Some(ref file) = log_file {
|
||||
let log_writer = OpenOptions::new()
|
||||
.write(true)
|
||||
.read(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&file)?;
|
||||
|
||||
// TODO: Support 'text' log format.
|
||||
let (logger_local, logger_async_guard_local) =
|
||||
logging::create_logger(crate_name!(), crate_name!(), log_level, log_writer);
|
||||
|
||||
Ok((logger_local, Some(logger_async_guard_local)))
|
||||
} else {
|
||||
let logger = slog::Logger::root(slog::Discard, o!());
|
||||
Ok((logger, None))
|
||||
}
|
||||
}
|
||||
|
||||
async fn real_main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let root_path = if let Some(path) = cli.global.root {
|
||||
path
|
||||
} else {
|
||||
PathBuf::from(DEFAULT_ROOT_DIR)
|
||||
};
|
||||
|
||||
let log_level = if cli.global.debug {
|
||||
slog::Level::Debug
|
||||
} else {
|
||||
DEFAULT_LOG_LEVEL
|
||||
};
|
||||
|
||||
let (logger, _async_guard) = setup_logger(cli.global.log, log_level)?;
|
||||
|
||||
cmd_run(cli.subcmd, &root_path, &logger).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
if let Err(e) = real_main().await {
|
||||
eprintln!("ERROR: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
exit(0);
|
||||
}
|
Loading…
Reference in New Issue
Block a user