use crate::{Config, nix::Nix, podman::Podman, process::Process, system::System};
use anyhow::{Result, bail};
use log::{LevelFilter, debug, info, trace};
use std::{
fmt::Display,
fs,
path::{Path, PathBuf},
process::{Command, Stdio},
};
const DEFAULT_IMAGE: &str = "kubernix:base";
const DEFAULT_ROOT: &str = "kubernix";
pub struct Container;
impl Container {
pub fn build(config: &Config) -> Result<()> {
System::find_executable(config.container_runtime())?;
let policy_json = Self::policy_json(config);
fs::write(&policy_json, include_str!("assets/policy.json"))?;
if !config.multi_node() {
return Ok(());
}
if Self::image_exists(config) {
info!(
"Container image '{}' already exists, skipping build",
DEFAULT_IMAGE
);
return Ok(());
}
info!("Building base container image '{}'", DEFAULT_IMAGE);
let file = config.root().join("Dockerfile");
if !file.exists() {
if let Some(custom) = config.dockerfile() {
debug!("Using custom Dockerfile '{}'", custom.display());
fs::copy(custom, &file)?;
} else {
fs::write(
&file,
format!(
include_str!("assets/Dockerfile"),
nix = Nix::DIR,
root = DEFAULT_ROOT
),
)?;
}
}
let dockerignore = config.root().join(".dockerignore");
if !dockerignore.exists() {
fs::write(&dockerignore, "nix/.git\n")?;
}
let mut args = if Podman::is_configured(config) {
Podman::build_args(config, &policy_json)?
} else {
vec!["build".into()]
};
args.extend(vec![format!("-t={}", DEFAULT_IMAGE), ".".into()]);
trace!("Container runtime build args: {:?}", args);
debug!("Running container runtime with args: {}", args.join(" "));
let status = Command::new(config.container_runtime())
.current_dir(config.root())
.args(args)
.stderr(Self::stdio(config))
.stdout(Self::stdio(config))
.status()?;
if !status.success() {
bail!(
"Unable to build container base image '{}' using '{}' (exit: {})",
DEFAULT_IMAGE,
config.container_runtime(),
status,
);
}
info!("Container base image built");
Ok(())
}
pub fn policy_json(config: &Config) -> PathBuf {
config.root().join("policy.json")
}
pub fn start(
config: &Config,
dir: &Path,
identifier: &str,
process_name: &str,
container_name: &str,
args: &[&str],
) -> Result<Process> {
Self::remove(config, container_name);
let arg_hostname = &format!("--hostname={}", container_name);
let arg_name = &format!("--name={}", Self::prefixed_container_name(container_name));
let arg_volume_root = &Self::volume_arg(config.root().display());
let mut args_vec = vec![
"run",
"--net=host",
"--privileged",
"--rm",
"--cgroupns=host",
arg_hostname,
arg_name,
arg_volume_root,
];
let podman_args = Podman::default_args(config)?;
if Podman::is_configured(config) {
args_vec.extend(podman_args.iter().map(|x| x.as_str()).collect::<Vec<_>>())
}
let dev_mapper = PathBuf::from("/").join("dev").join("mapper");
let arg_volume_dev_mapper = &Self::volume_arg(dev_mapper.display());
if dev_mapper.exists() {
args_vec.push(arg_volume_dev_mapper);
}
args_vec.extend(&[DEFAULT_IMAGE, process_name]);
args_vec.extend(args);
trace!("Container runtime start args: {:?}", args_vec);
Process::start(dir, identifier, config.container_runtime(), &args_vec)
}
fn volume_arg<T: Display>(volume: T) -> String {
format!("--volume={v}:{v}", v = volume)
}
pub fn exec(
config: &Config,
dir: &Path,
identifier: &str,
process_name: &str,
container_name: &str,
args: &[&str],
) -> Result<Process> {
let mut args_vec = vec![];
let podman_args = Podman::default_args(config)?;
if Podman::is_configured(config) {
args_vec.extend(podman_args.iter().map(|x| x.as_str()).collect::<Vec<_>>())
}
let name = Self::prefixed_container_name(container_name);
let flake_ref = format!("path:{}", DEFAULT_ROOT);
let mut cmd_parts = vec![process_name.to_string()];
cmd_parts.extend(args.iter().map(|a| a.to_string()));
let run_cmd = cmd_parts.join(" ");
args_vec.extend(vec![
"exec",
&name,
"nix",
"develop",
&flake_ref,
"--no-update-lock-file",
"--command",
"bash",
"-c",
&run_cmd,
]);
trace!("Container runtime exec args: {:?}", args_vec);
Process::start(dir, identifier, config.container_runtime(), &args_vec)
}
fn remove(config: &Config, name: &str) {
let prefixed = Self::prefixed_container_name(name);
match Command::new(config.container_runtime())
.arg("rm")
.arg("-f")
.arg(&prefixed)
.stderr(Stdio::null())
.stdout(Stdio::null())
.status()
{
Ok(status) if !status.success() => {
debug!("Container '{}' removal exited with {}", prefixed, status);
}
Err(e) => {
debug!("Failed to run container rm for '{}': {}", prefixed, e);
}
_ => {}
}
}
fn stdio(config: &Config) -> Stdio {
if config.log_level() > LevelFilter::Info {
Stdio::inherit()
} else {
Stdio::null()
}
}
fn image_exists(config: &Config) -> bool {
Command::new(config.container_runtime())
.args(["image", "inspect", DEFAULT_IMAGE])
.stderr(Stdio::null())
.stdout(Stdio::null())
.status()
.is_ok_and(|s| s.success())
}
fn prefixed_container_name(name: &str) -> String {
format!("{}-{}", DEFAULT_ROOT, name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prefixed_container_name_format() {
assert_eq!(
Container::prefixed_container_name("node-0"),
"kubernix-node-0"
);
}
#[test]
fn volume_arg_format() {
assert_eq!(
Container::volume_arg("/some/path"),
"--volume=/some/path:/some/path"
);
}
#[test]
fn policy_json_path() {
let c = crate::config::tests::test_config().unwrap();
let path = Container::policy_json(&c);
assert!(path.ends_with("policy.json"));
}
}