reno 0.1.0

Implementation of the Open Container Initiative Runtime Specification
Documentation
use crate::container::fork;
use crate::hook;
use crate::socket::{SocketClient, SocketServer};
use crate::state::State;
use crate::state::Status;

use anyhow::{bail, Context, Result};
use clap::{Parser, Subcommand};
use nix::sys::signal::{self, Signal};
use nix::unistd::Pid;
use oci_spec::runtime::Spec;
use std::fs;
use std::path::Path;

const RENO_ROOT: &str = "/tmp/reno";

#[derive(Parser, Debug)]
#[clap(version, about)]
pub struct Cli {
    #[command(subcommand)]
    pub command: CliSubcommand,
}

#[derive(Subcommand, Debug)]
pub enum CliSubcommand {
    #[command(about = "print the state of a container")]
    State { id: String },

    #[command(about = "create a container")]
    Create {
        id: String,

        #[arg(long)]
        bundle: String,

        #[arg(long)]
        pid_file: Option<String>,
    },

    #[command(about = "start a container")]
    Start { id: String },

    #[command(about = "kill a container")]
    Kill { id: String, signal: String },

    #[command(about = "delete a container")]
    Delete { id: String },
}

pub fn state(id: &str) -> Result<()> {
    let container_root = Path::new(RENO_ROOT).join(id);
    let mut state = State::load(&container_root)?;
    state.refresh();

    let serialized_state =
        serde_json::to_string(&state).context("failed to serialize the state")?;
    println!("{}", serialized_state);

    state.persist(&container_root)?;
    Ok(())
}

pub fn create(id: &str, bundle: &str, pid_file: &Option<String>) -> Result<()> {
    let bundle = Path::new(bundle);
    let bundle_exists = bundle
        .try_exists()
        .context("failed to check if the bundle exists")?;
    if !bundle_exists {
        bail!("the bundle doesn't exist");
    }

    let bundle_spec = bundle.join("config.json");
    let spec = Spec::load(bundle_spec).context("failed to load the bundle configuration")?;

    let container_root = Path::new(RENO_ROOT).join(id);
    let container_root_exists = container_root
        .try_exists()
        .context("failed to check if the container exists")?;
    if container_root_exists {
        bail!("the container exists");
    }

    fs::create_dir_all(&container_root).context("failed to create the container root path")?;

    let mut state = State::new(id.to_string(), bundle.to_path_buf());
    state.persist(&container_root)?;

    let namespaces = match &spec.linux() {
        Some(linux) => linux.namespaces().clone().unwrap_or_default(),
        None => Vec::new(),
    };

    let init_socket_path = container_root.join("init.sock");
    let mut init_socket_server = SocketServer::bind(&init_socket_path)?;

    let container_socket_path = container_root.join("container.sock");
    let pid = fork::fork_container(
        &spec,
        &state,
        &namespaces,
        &init_socket_path,
        &container_socket_path,
    )?;

    init_socket_server.listen()?;

    let mut container_socket_client = SocketClient::connect(&container_socket_path)?;
    let container_message = container_socket_client.read()?;
    container_socket_client.shutdown()?;

    if container_message.status == Status::Creating {
        if let Some(hooks) = spec.hooks() {
            if let Some(create_runtime_hooks) = hooks.create_runtime() {
                for create_runtime_hook in create_runtime_hooks {
                    hook::run_hook(&state, create_runtime_hook)
                        .context("failed to invoke the create_runtime hook")?;
                }
            }
        }
    } else if let Some(error) = container_message.error {
        bail!("failed to create the container: {}", error);
    } else {
        bail!("failed to create the container");
    }

    let mut container_socket_client = SocketClient::connect(&container_socket_path)?;
    let container_message = container_socket_client.read()?;
    container_socket_client.shutdown()?;

    if container_message.status == Status::Created {
        state.pid = pid.as_raw();
        state.status = Status::Created;
        state.persist(&container_root)?;
        if let Some(pid_file) = pid_file {
            state.write_pid_file(Path::new(pid_file))?;
        }
        Ok(())
    } else if let Some(error) = container_message.error {
        bail!("failed to create the container: {}", error);
    } else {
        bail!("failed to create the container");
    }
}

pub fn start(id: &str) -> Result<()> {
    let container_root = Path::new(RENO_ROOT).join(id);
    container_root
        .try_exists()
        .context("the container doesn't exist")?;

    let mut state = State::load(&container_root)?;
    if state.status != Status::Created {
        bail!("the container is not in the 'Created' state");
    }

    let bundle_spec = state.bundle.join("config.json");
    let spec = Spec::load(bundle_spec).context("failed to load the bundle configuration")?;

    if let Some(hooks) = spec.hooks() {
        if let Some(pre_start_hooks) = hooks.prestart() {
            for pre_start_hook in pre_start_hooks {
                hook::run_hook(&state, pre_start_hook)
                    .context("failed to invoke the pre_start hook")?;
            }
        }
    }

    let container_socket_path = container_root.join("container.sock");
    let mut container_socket_client = SocketClient::connect(&container_socket_path)?;
    let container_message = container_socket_client.read()?;
    container_socket_client.shutdown()?;

    if container_message.status == Status::Running {
        state.refresh();
        state.persist(&container_root)?;

        if let Some(hooks) = spec.hooks() {
            if let Some(post_start_hooks) = hooks.poststart() {
                for post_start_hook in post_start_hooks {
                    hook::run_hook(&state, post_start_hook)
                        .context("failed to invoke the post_start hook")?;
                }
            }
        }
        Ok(())
    } else if let Some(error) = container_message.error {
        bail!("failed to start the container: {}", error);
    } else {
        bail!("failed to start the container");
    }
}

pub fn kill(id: &str, signal: &str) -> Result<()> {
    let container_root = Path::new(RENO_ROOT).join(id);
    container_root
        .try_exists()
        .context("the container doesn't exist")?;

    let mut state = State::load(&container_root)?;
    if state.status != Status::Created && state.status != Status::Running {
        bail!("the container is not in the 'Created' or 'Running' state");
    }

    let signal = match signal {
        "HUP" => Signal::SIGHUP,
        "INT" => Signal::SIGINT,
        "TERM" => Signal::SIGTERM,
        "STOP" => Signal::SIGSTOP,
        "KILL" => Signal::SIGKILL,
        "USR1" => Signal::SIGUSR1,
        "USR2" => Signal::SIGUSR2,
        _ => Signal::SIGKILL,
    };

    let pid = Pid::from_raw(state.pid);
    signal::kill(pid, signal).context("failed to kill the container")?;

    state.refresh();
    state.persist(&container_root)?;
    Ok(())
}

pub fn delete(id: &str) -> Result<()> {
    let container_root = Path::new(RENO_ROOT).join(id);
    container_root
        .try_exists()
        .context("the container doesn't exist")?;

    let state = State::load(&container_root)?;
    if state.status != Status::Stopped {
        bail!("the container is not in the 'Stopped' state");
    }

    fs::remove_dir_all(container_root).context("failed to remove the container")?;

    let bundle_spec = state.bundle.join("config.json");
    let spec = Spec::load(bundle_spec).context("failed to load the bundle configuration")?;
    if let Some(hooks) = spec.hooks() {
        if let Some(post_stop_hooks) = hooks.poststop() {
            for post_stop_hook in post_stop_hooks {
                hook::run_hook(&state, post_stop_hook)
                    .context("failed to invoke the post_stop hook")?;
            }
        }
    }
    Ok(())
}